sui: redesign Wormhole and Token Bridge contracts
This commit is contained in:
parent
67c7c86419
commit
760db3c810
|
@ -207,16 +207,12 @@ jobs:
|
|||
sui:
|
||||
name: Sui
|
||||
runs-on: ubuntu-20.04
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
working-directory: ./sui
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Run tests via docker
|
||||
run: make test-docker
|
||||
run: cd sui && make test-docker
|
||||
|
||||
terra:
|
||||
runs-on: ubuntu-20.04
|
||||
|
|
|
@ -31,3 +31,5 @@ bigtable-writer.json
|
|||
/solana/artifacts-mainnet/
|
||||
/ethereum/out/
|
||||
/ethereum/cache/
|
||||
sui.log.*
|
||||
sui/examples/wrapped_coin
|
||||
|
|
|
@ -47,6 +47,7 @@ COPY --from=const-build /scripts/.env.hex terra/tools/.env
|
|||
COPY --from=const-build /scripts/.env.hex cosmwasm/deployment/terra2/tools/.env
|
||||
COPY --from=const-build /scripts/.env.hex algorand/.env
|
||||
COPY --from=const-build /scripts/.env.hex near/.env
|
||||
COPY --from=const-build /scripts/.env.hex sui/.env
|
||||
COPY --from=const-build /scripts/.env.hex aptos/.env
|
||||
COPY --from=const-build /scripts/.env.hex wormchain/contracts/tools/.env
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
env.sh
|
||||
deploy.out
|
||||
sui.log.*
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
# first build the image
|
||||
(cd ..; DOCKER_BUILDKIT=1 docker build --progress plain -f sui/Dockerfile.base -t sui .)
|
||||
|
||||
cd ..; DOCKER_BUILDKIT=1 docker build --no-cache --progress plain -f sui/Dockerfile.base -t sui .
|
||||
|
||||
# tag the image with the appropriate version
|
||||
docker tag sui:latest ghcr.io/wormhole-foundation/sui:0.15.0
|
||||
|
||||
docker tag sui:latest ghcr.io/wormhole-foundation/sui:1.0.0-testnet
|
||||
|
||||
# push to ghcr
|
||||
docker push ghcr.io/wormhole-foundation/sui:0.15.0
|
||||
|
||||
docker push ghcr.io/wormhole-foundation/sui:1.0.0-testnet
|
||||
|
||||
echo remember to update both Dockerfile and Dockerfile.export
|
||||
|
|
|
@ -1,25 +1,33 @@
|
|||
FROM ghcr.io/wormhole-foundation/sui:0.21.1@sha256:59b91529e426b44c152b40ad0e7a6a7aafc8225722b5d7e331056a4d65845015 as sui
|
||||
FROM ghcr.io/wormhole-foundation/sui:1.0.0-testnet@sha256:63a8094590ddb90320aa1c86414f17cc73c759ecbdfaf2fe78f135b7c08ec536 as sui
|
||||
|
||||
RUN dnf -y install make git
|
||||
RUN dnf -y install make git npm
|
||||
|
||||
COPY README.md cert.pem* /certs/
|
||||
COPY sui/README.md sui/cert.pem* /certs/
|
||||
RUN if [ -e /certs/cert.pem ]; then cp /certs/cert.pem /etc/ssl/certs/ca-certificates.crt; fi
|
||||
RUN if [ -e /certs/cert.pem ]; then git config --global http.sslCAInfo /certs/cert.pem; fi
|
||||
|
||||
RUN sui genesis -f
|
||||
|
||||
COPY sui/devnet/ /root/.sui/sui_config/
|
||||
|
||||
# Build CLI, TODO(aki): move this to base image before merging into main
|
||||
RUN npm install -g n typescript ts-node
|
||||
RUN n stable
|
||||
COPY clients/js /tmp/clients/js
|
||||
WORKDIR /tmp/clients/js
|
||||
RUN make install
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
RUN sui genesis -f
|
||||
COPY scripts/start_node.sh .
|
||||
COPY scripts/funder.sh .
|
||||
|
||||
COPY wormhole/ wormhole
|
||||
COPY token_bridge/ token_bridge
|
||||
# COPY examples/ examples
|
||||
COPY Makefile Makefile
|
||||
COPY sui/scripts/ scripts
|
||||
COPY sui/wormhole/ wormhole
|
||||
COPY sui/token_bridge/ token_bridge
|
||||
COPY sui/examples/ examples
|
||||
COPY sui/Makefile Makefile
|
||||
COPY sui/.env* .
|
||||
|
||||
FROM sui AS tests
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
RUN --mount=type=cache,target=/root/.move,id=move_cache \
|
||||
make test
|
||||
RUN --mount=type=cache,target=/root/.move,id=move_cache make test
|
||||
|
|
|
@ -13,10 +13,11 @@ COPY sui/scripts/node_builder.sh /tmp
|
|||
|
||||
RUN /tmp/node_builder.sh
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
FROM docker.io/redhat/ubi8@sha256:56c374376a42da40f3aec753c4eab029b5ea162d70cb5f0cda24758780c31d81 as export-stage
|
||||
|
||||
RUN dnf -y update
|
||||
RUN dnf -y install jq curl git
|
||||
|
||||
COPY --from=sui-node /root/.cargo/bin/sui /bin/sui
|
||||
COPY --from=sui-node /root/.cargo/bin/sui-faucet /bin/sui-faucet
|
||||
COPY --from=sui-node /root/.cargo/bin/sui-node /bin/sui-node
|
||||
|
|
18
sui/Makefile
18
sui/Makefile
|
@ -1,13 +1,13 @@
|
|||
CONTRACT_DIRS := wormhole token_bridge
|
||||
TEST_CONTRACT_DIRS := wormhole token_bridge examples/coins examples/core_messages
|
||||
CLEAN_CONTRACT_DIRS := wormhole token_bridge examples/coins examples/core_messages
|
||||
|
||||
TARGETS := build test
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(foreach dir,$(TEST_CONTRACT_DIRS), make -C $(dir) $@ &&) true
|
||||
|
||||
.PHONY: $(TARGETS)
|
||||
$(TARGETS):
|
||||
$(foreach dir,$(CONTRACT_DIRS), make -C $(dir) $@ &&) true
|
||||
.PHONY: test
|
||||
test:
|
||||
$(foreach dir,$(TEST_CONTRACT_DIRS), make -C $(dir) $@ &&) true
|
||||
|
||||
test-docker:
|
||||
DOCKER_BUILDKIT=1 docker build -f Dockerfile --target tests .
|
||||
|
||||
sui_export:
|
||||
DOCKER_BUILDKIT=1 docker build --progress plain -f Dockerfile.export -t near-export -o type=local,dest=$$HOME/.cargo/bin .
|
||||
DOCKER_BUILDKIT=1 docker build -f Dockerfile ..
|
||||
|
|
|
@ -110,3 +110,5 @@ Digest: cL+uWFEVcQrkAiOxOJmaK7JmlOJdE3/8X5JFbJwBxCQ=
|
|||
kubectl exec -it guardian-0 -- /guardiand admin send-observation-request --socket /tmp/admin.sock 21 70bfae585115710ae40223b138999a2bb26694e25d137ffc5f92456c9c01c424
|
||||
|
||||
// curl -s -X POST -d '{"jsonrpc":"2.0", "id": 1, "method": "sui_getCommitteeInfo", "params": []}' -H 'Content-Type: application/json' http://127.0.0.1:9002 | jq
|
||||
|
||||
// curl -s -X POST -d '{"jsonrpc":"2.0", "id": 1, "method": "sui_getLatestCheckpointSequenceNumber", "params": []}' -H 'Content-Type: application/json' http://127.0.0.1:9000
|
||||
|
|
141
sui/README.md
141
sui/README.md
|
@ -1,14 +1,137 @@
|
|||
# Wormhole on Sui
|
||||
|
||||
This folder contains the reference implementation of the Wormhole cross-chain
|
||||
messaging protocol smart contracts on the [Sui](https://mystenlabs.com/)
|
||||
blockchain, implemented in the [Move](https://move-book.com/) programming
|
||||
language.
|
||||
|
||||
# Project structure
|
||||
|
||||
The project is laid out as follows:
|
||||
|
||||
- [wormhole](./wormhole) the core messaging layer
|
||||
- [token_bridge](./token_bridge) the asset transfer layer
|
||||
- [coin](./coin) a template for creating Wormhole wrapped coins
|
||||
|
||||
# Installation
|
||||
Make sure your Cargo version is at least 1.64.0 and then follow the steps below:
|
||||
|
||||
Make sure your Cargo version is at least 1.65.0 and then follow the steps below:
|
||||
|
||||
- https://docs.sui.io/build/install
|
||||
|
||||
## Prerequisites
|
||||
|
||||
# Sui CLI
|
||||
- do `sui start` to spin up a local network
|
||||
- do `rpc-server` to start a server for handling rpc calls
|
||||
- do `sui-faucet` to start a faucet for requesting funds from active-address
|
||||
Install the `Sui` CLI. This tool is used to compile the contracts and run the tests.
|
||||
|
||||
# TODOs
|
||||
- The move dependencies are currently pinned to a version that matches the
|
||||
docker image for reproducibility. These should be regularly updated to track
|
||||
any upstream changes before the mainnet release.
|
||||
```sh
|
||||
cargo install --locked --git https://github.com/MystenLabs/sui.git --rev 09b2081498366df936abae26eea4b2d5cafb2788 sui sui-faucet
|
||||
```
|
||||
|
||||
Some useful Sui CLI commands are
|
||||
|
||||
- `sui start` to spin up a local network
|
||||
- `rpc-server` to start a server for handling rpc calls
|
||||
- `sui-faucet` to start a faucet for requesting funds from active-address
|
||||
|
||||
Next, install the [worm](../clients/js/README.md) CLI tool by running
|
||||
|
||||
```sh
|
||||
wormhole/clients/js $ make install
|
||||
```
|
||||
|
||||
`worm` is the swiss army knife for interacting with wormhole contracts on all
|
||||
supported chains, and generating signed messages (VAAs) for testing.
|
||||
|
||||
As an optional, but recommended step, install the
|
||||
[move-analyzer](https://github.com/move-language/move/tree/main/language/move-analyzer)
|
||||
Language Server (LSP):
|
||||
|
||||
```sh
|
||||
cargo install --git https://github.com/move-language/move.git move-analyzer --branch main --features "address32"
|
||||
```
|
||||
|
||||
This installs the LSP backend which is then supported by most popular editors such as [emacs](https://github.com/emacs-lsp/lsp-mode), [vim](https://github.com/neoclide/coc.nvim), and even [vscode](https://marketplace.visualstudio.com/items?itemName=move.move-analyzer).
|
||||
|
||||
<details>
|
||||
<summary>For emacs, you may need to add the following to your config file:</summary>
|
||||
|
||||
```lisp
|
||||
;; Move
|
||||
(define-derived-mode move-mode rust-mode "Move"
|
||||
:group 'move-mode)
|
||||
|
||||
(add-to-list 'auto-mode-alist '("\\.move\\'" . move-mode))
|
||||
|
||||
(with-eval-after-load 'lsp-mode
|
||||
(add-to-list 'lsp-language-id-configuration
|
||||
'(move-mode . "move"))
|
||||
|
||||
(lsp-register-client
|
||||
(make-lsp-client :new-connection (lsp-stdio-connection "move-analyzer")
|
||||
:activation-fn (lsp-activate-on "move")
|
||||
:server-id 'move-analyzer)))
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Building & running tests
|
||||
|
||||
The project uses a simple `make`-based build system for building and running
|
||||
tests. Running `make test` in this directory will run the tests for each
|
||||
contract. If you only want to run the tests for, say, the token bridge contract,
|
||||
then you can run `make test` in the `token_bridge` directory, or run `make -C
|
||||
token_bridge test` from this directory.
|
||||
|
||||
Additionally, `make test-docker` runs the tests in a docker container which is
|
||||
set up with all the necessary dependencies. This is the command that runs in CI.
|
||||
|
||||
## Running a local validator and deploying the contracts to it
|
||||
|
||||
Simply run
|
||||
|
||||
```sh
|
||||
worm start-validator sui
|
||||
```
|
||||
|
||||
which will start a local sui validator with an RPC endpoint at `0.0.0.0:9000`
|
||||
and the faucet endpoint at `0.0.0.0:5003/gas`. Note that the faucet takes a few
|
||||
(~10) seconds to come up, so only proceed when you see the following:
|
||||
|
||||
```text
|
||||
Faucet is running. Faucet endpoint: 0.0.0.0:5003/gas
|
||||
```
|
||||
|
||||
Once the validator is running, the contracts are ready to deploy. In the
|
||||
[scripts](./scripts) directory, run
|
||||
|
||||
```sh
|
||||
scripts $ ./deploy.sh devnet
|
||||
```
|
||||
|
||||
This will deploy the core contract and the token bridge.
|
||||
|
||||
When you make a change to the contract, you can simply restart the validator and
|
||||
run the deploy script again.
|
||||
|
||||
<!-- However, a better way is to run one of the following scripts:
|
||||
|
||||
``` sh
|
||||
scripts $ ./upgrade devnet Core # for upgrading the wormhole contract
|
||||
scripts $ ./upgrade devnet TokenBridge # for upgarding the token bridge contract
|
||||
scripts $ ./upgrade devnet NFTBridge # for upgarding the NFT bridge contract
|
||||
```
|
||||
|
||||
Behind the scenes, these scripts exercise the whole contract upgrade code path
|
||||
(see below), including generating and verifying a signed governance action, and
|
||||
the Move bytecode verifier checking ABI compatibility. If an upgrade here fails
|
||||
due to incompatibility, it will likely on mainnet too. (TODO: add CI action to
|
||||
simulate upgrades against main when there's a stable version) -->
|
||||
|
||||
# Implementation notes / coding practices
|
||||
|
||||
In this section, we describe some of the implementation design decisions and
|
||||
coding practices we converged on along the way. Note that the coding guidelines
|
||||
are prescriptive rather than descriptive, and the goal is for the contracts to
|
||||
ultimately follow these, but they might not during earlier development phases.
|
||||
|
||||
### TODO
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
[package]
|
||||
name = "Coin"
|
||||
version = "0.0.1"
|
||||
|
||||
[dependencies]
|
||||
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "2d709054a08d904b9229a2472af679f210af3827" }
|
||||
TokenBridge = { local = "../token_bridge"}
|
||||
Wormhole = { local = "../wormhole"}
|
||||
|
||||
[addresses]
|
||||
coin="0x0"
|
|
@ -1,24 +0,0 @@
|
|||
module coin::coin {
|
||||
use sui::transfer;
|
||||
use sui::tx_context::{Self, TxContext};
|
||||
|
||||
use token_bridge::wrapped;
|
||||
|
||||
struct COIN has drop {}
|
||||
|
||||
fun init(coin_witness: COIN, ctx: &mut TxContext) {
|
||||
// Step 1. Paste token attestation VAA below.
|
||||
let vaa_bytes = x"0100000000010080366065746148420220f25a6275097370e8db40984529a6676b7a5fc9feb11755ec49ca626b858ddfde88d15601f85ab7683c5f161413b0412143241c700aff010000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef000000000150eb23000200000000000000000000000000000000000000000000000000000000beefface00020c424545460000000000000000000000000000000000000000000000000000000042656566206661636520546f6b656e0000000000000000000000000000000000";
|
||||
|
||||
let new_wrapped_coin = wrapped::create_wrapped_coin(vaa_bytes, coin_witness, ctx);
|
||||
transfer::transfer(
|
||||
new_wrapped_coin,
|
||||
tx_context::sender(ctx)
|
||||
);
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun test_init(ctx: &mut TxContext) {
|
||||
init(COIN {}, ctx)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
keystore:
|
||||
File: /root/.sui/sui_config/sui.keystore
|
||||
envs:
|
||||
- alias: localnet
|
||||
rpc: "http://0.0.0.0:9000"
|
||||
ws: ~
|
||||
- alias: devnet
|
||||
rpc: "https://fullnode.devnet.sui.io:443"
|
||||
ws: ~
|
||||
active_env: localnet
|
||||
active_address: "0xed867315e3f7c83ae82e6d5858b6a6cc57c291fd84f7509646ebc8162169cf96"
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: U/JD+ELChkDDYiw/jq+I1RVJAUE8Kcu8FiQBSDVM7Aw=
|
||||
worker-key-pair:
|
||||
value: AAw8S7FtL0j0VTRKWNrL/GJCB8BwJOA6I2N0OGW6QlG8
|
||||
account-key-pair:
|
||||
value: AFyoDGvHw/O4uSwf+qXg96bbRan1T0v6Zmnv+P8brtGE
|
||||
network-key-pair:
|
||||
value: AH8ybx8kJxx1VDebF9aJv2pQQqWoDM9tnkHnPUo5EFwS
|
||||
db-path: /root/.sui/sui_config/authorities_db/full_node_db
|
||||
network-address: /ip4/127.0.0.1/tcp/32889/http
|
||||
json-rpc-address: "0.0.0.0:9000"
|
||||
metrics-address: "127.0.0.1:43249"
|
||||
admin-interface-port: 36887
|
||||
enable-event-processing: true
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: ~
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:34131"
|
||||
external-address: /ip4/127.0.0.1/udp/34131
|
||||
seed-peers:
|
||||
- peer-id: 68f5be330e0db85dccbdc36d4e1c59dd2828a27365db26a75198c8a54335b1de
|
||||
address: /ip4/127.0.0.1/udp/33745
|
||||
- peer-id: 0b75d3508725b9d260995e7081e9ff3639d7c7d858f8d2379cc9c9f39d8dfc8a
|
||||
address: /ip4/127.0.0.1/udp/40075
|
||||
- peer-id: 6447bdd9b1bf65a16036950d7ba7a495247ecb291c55b113ed3e76d0543196d9
|
||||
address: /ip4/127.0.0.1/udp/39953
|
||||
- peer-id: f454efea06dadd727a682d0b08d55f2df32b83e364a0b75270aaf9c247f0ed6f
|
||||
address: /ip4/127.0.0.1/udp/35253
|
||||
genesis:
|
||||
genesis-file-location: /root/.sui/sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 1
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 10
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
"AB522qKKEsXMTFRD2SG3Het/02S/ZBOugmcH3R1CDG6l",
|
||||
"AOmPq9B16F3W3ijO/4s9hI6v8LdiYCawKAW31PKpg4Qp",
|
||||
"AOLhc0ryVWnD5LmqH3kCHruBpVV+68EWjEGu2eC9gndK",
|
||||
"AKCo1FyhQ0zUpnoZLmGJJ+8LttTrt56W87Ho4vBF+R+8",
|
||||
"AGA20wtGcwbcNAG4nwapbQ5wIuXwkYQEWFUoSVAxctHb"
|
||||
]
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: VTDx4HjVmRBqdqBWg2zN+zcFE20io3CrBchGy/iV1lo=
|
||||
worker-key-pair:
|
||||
value: AOALyRbJi6ld82KFPzzSTqFcvnEJe7cxFmE4UmgTMkt1
|
||||
account-key-pair:
|
||||
value: AFO6/i9VpgpR9Tb5fA7PBh2bM8HMbsxjPC/twrRFXZoh
|
||||
network-key-pair:
|
||||
value: ACTXLjvKVNOmez4eB/undu829MFIpNqRactays+pGNnf
|
||||
db-path: /root/.sui/sui_config/authorities_db/99f25ef61f80
|
||||
network-address: /ip4/127.0.0.1/tcp/37395/http
|
||||
json-rpc-address: "127.0.0.1:34677"
|
||||
metrics-address: "127.0.0.1:34733"
|
||||
admin-interface-port: 33561
|
||||
consensus-config:
|
||||
address: /ip4/127.0.0.1/tcp/36643/http
|
||||
db-path: /root/.sui/sui_config/consensus_db/99f25ef61f80
|
||||
internal-worker-address: ~
|
||||
max-pending-transactions: ~
|
||||
narwhal-config:
|
||||
header_num_of_batches_threshold: 32
|
||||
max_header_num_of_batches: 1000
|
||||
max_header_delay: 2000ms
|
||||
min_header_delay: 500ms
|
||||
gc_depth: 50
|
||||
sync_retry_delay: 5000ms
|
||||
sync_retry_nodes: 3
|
||||
batch_size: 500000
|
||||
max_batch_delay: 100ms
|
||||
block_synchronizer:
|
||||
range_synchronize_timeout: 30000ms
|
||||
certificates_synchronize_timeout: 30000ms
|
||||
payload_synchronize_timeout: 30000ms
|
||||
payload_availability_timeout: 30000ms
|
||||
handler_certificate_deliver_timeout: 30000ms
|
||||
consensus_api_grpc:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/32825/http
|
||||
get_collections_timeout: 5000ms
|
||||
remove_collections_timeout: 5000ms
|
||||
max_concurrent_requests: 500000
|
||||
prometheus_metrics:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/43711/http
|
||||
network_admin_server:
|
||||
primary_network_admin_server_port: 34257
|
||||
worker_network_admin_server_base_port: 45379
|
||||
anemo:
|
||||
send_certificate_rate_limit: ~
|
||||
get_payload_availability_rate_limit: ~
|
||||
get_certificates_rate_limit: ~
|
||||
report_batch_rate_limit: ~
|
||||
request_batch_rate_limit: ~
|
||||
enable-event-processing: false
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: 20000000000
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:33745"
|
||||
external-address: /ip4/127.0.0.1/udp/33745
|
||||
genesis:
|
||||
genesis-file-location: /root/.sui/sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 1
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 10
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: avYcyVgYMXTyaUYh9IRwLK0gSzl7YF6ZQDAbrS1Bhvo=
|
||||
worker-key-pair:
|
||||
value: ALcYC9nZa2UFKkxycem4wHZUW6nPTmAPWIC0Me/X8/OQ
|
||||
account-key-pair:
|
||||
value: AFQ60/bLdbiryFJsWRrXW29RvC56WN2CAyS75jTRtQWj
|
||||
network-key-pair:
|
||||
value: AKgyYvFpPmPmmEPNdltJ4cfcb9D0t0bigFz3cak+iblf
|
||||
db-path: /root/.sui/sui_config/authorities_db/8dcff6d15504
|
||||
network-address: /ip4/127.0.0.1/tcp/45631/http
|
||||
json-rpc-address: "127.0.0.1:39065"
|
||||
metrics-address: "127.0.0.1:36781"
|
||||
admin-interface-port: 41085
|
||||
consensus-config:
|
||||
address: /ip4/127.0.0.1/tcp/46119/http
|
||||
db-path: /root/.sui/sui_config/consensus_db/8dcff6d15504
|
||||
internal-worker-address: ~
|
||||
max-pending-transactions: ~
|
||||
narwhal-config:
|
||||
header_num_of_batches_threshold: 32
|
||||
max_header_num_of_batches: 1000
|
||||
max_header_delay: 2000ms
|
||||
min_header_delay: 500ms
|
||||
gc_depth: 50
|
||||
sync_retry_delay: 5000ms
|
||||
sync_retry_nodes: 3
|
||||
batch_size: 500000
|
||||
max_batch_delay: 100ms
|
||||
block_synchronizer:
|
||||
range_synchronize_timeout: 30000ms
|
||||
certificates_synchronize_timeout: 30000ms
|
||||
payload_synchronize_timeout: 30000ms
|
||||
payload_availability_timeout: 30000ms
|
||||
handler_certificate_deliver_timeout: 30000ms
|
||||
consensus_api_grpc:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/36797/http
|
||||
get_collections_timeout: 5000ms
|
||||
remove_collections_timeout: 5000ms
|
||||
max_concurrent_requests: 500000
|
||||
prometheus_metrics:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/40191/http
|
||||
network_admin_server:
|
||||
primary_network_admin_server_port: 34921
|
||||
worker_network_admin_server_base_port: 43785
|
||||
anemo:
|
||||
send_certificate_rate_limit: ~
|
||||
get_payload_availability_rate_limit: ~
|
||||
get_certificates_rate_limit: ~
|
||||
report_batch_rate_limit: ~
|
||||
request_batch_rate_limit: ~
|
||||
enable-event-processing: false
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: 20000000000
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:40075"
|
||||
external-address: /ip4/127.0.0.1/udp/40075
|
||||
genesis:
|
||||
genesis-file-location: /root/.sui/sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 1
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 10
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: OXnx3yM1C/ppgnDMx/o1d49fJs7E05kq11mXNae/O+I=
|
||||
worker-key-pair:
|
||||
value: AFNNzJwFj3Xdc6SoCzXZ+8otMB5HXVBX/57G9JRkOJcJ
|
||||
account-key-pair:
|
||||
value: AIRtltXdA0Y0eLXY9eLpXFpZOcvP3HW9bzBnpVQMZs6Q
|
||||
network-key-pair:
|
||||
value: AOSqdS3WsvNdxqIZtKCK31wTPM5AqSkpDLkd56ZL/u+G
|
||||
db-path: /root/.sui/sui_config/authorities_db/addeef94d898
|
||||
network-address: /ip4/127.0.0.1/tcp/34255/http
|
||||
json-rpc-address: "127.0.0.1:40709"
|
||||
metrics-address: "127.0.0.1:43007"
|
||||
admin-interface-port: 45245
|
||||
consensus-config:
|
||||
address: /ip4/127.0.0.1/tcp/41199/http
|
||||
db-path: /root/.sui/sui_config/consensus_db/addeef94d898
|
||||
internal-worker-address: ~
|
||||
max-pending-transactions: ~
|
||||
narwhal-config:
|
||||
header_num_of_batches_threshold: 32
|
||||
max_header_num_of_batches: 1000
|
||||
max_header_delay: 2000ms
|
||||
min_header_delay: 500ms
|
||||
gc_depth: 50
|
||||
sync_retry_delay: 5000ms
|
||||
sync_retry_nodes: 3
|
||||
batch_size: 500000
|
||||
max_batch_delay: 100ms
|
||||
block_synchronizer:
|
||||
range_synchronize_timeout: 30000ms
|
||||
certificates_synchronize_timeout: 30000ms
|
||||
payload_synchronize_timeout: 30000ms
|
||||
payload_availability_timeout: 30000ms
|
||||
handler_certificate_deliver_timeout: 30000ms
|
||||
consensus_api_grpc:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/36241/http
|
||||
get_collections_timeout: 5000ms
|
||||
remove_collections_timeout: 5000ms
|
||||
max_concurrent_requests: 500000
|
||||
prometheus_metrics:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/36547/http
|
||||
network_admin_server:
|
||||
primary_network_admin_server_port: 38113
|
||||
worker_network_admin_server_base_port: 45711
|
||||
anemo:
|
||||
send_certificate_rate_limit: ~
|
||||
get_payload_availability_rate_limit: ~
|
||||
get_certificates_rate_limit: ~
|
||||
report_batch_rate_limit: ~
|
||||
request_batch_rate_limit: ~
|
||||
enable-event-processing: false
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: 20000000000
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:39953"
|
||||
external-address: /ip4/127.0.0.1/udp/39953
|
||||
genesis:
|
||||
genesis-file-location: /root/.sui/sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 1
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 10
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: CyNkjqNVr3HrHTH7f/NLs7u5lUHJzuPAw0PqMTD2y2s=
|
||||
worker-key-pair:
|
||||
value: ACKCL1GI/NLZHFVW1cUR6gu9fdh5ll/XpJWCRirrVDgj
|
||||
account-key-pair:
|
||||
value: AI5hzubOtOnMiBuXhZ2Cvnop5AQOxMHxhUdyPiy7/C37
|
||||
network-key-pair:
|
||||
value: AG5LdZdKNOxwTtshbJjyRcq3dJIHu9NQnBz4WwbQPl3K
|
||||
db-path: /root/.sui/sui_config/authorities_db/b3fd5efb5c87
|
||||
network-address: /ip4/127.0.0.1/tcp/43597/http
|
||||
json-rpc-address: "127.0.0.1:40123"
|
||||
metrics-address: "127.0.0.1:40221"
|
||||
admin-interface-port: 44653
|
||||
consensus-config:
|
||||
address: /ip4/127.0.0.1/tcp/39661/http
|
||||
db-path: /root/.sui/sui_config/consensus_db/b3fd5efb5c87
|
||||
internal-worker-address: ~
|
||||
max-pending-transactions: ~
|
||||
narwhal-config:
|
||||
header_num_of_batches_threshold: 32
|
||||
max_header_num_of_batches: 1000
|
||||
max_header_delay: 2000ms
|
||||
min_header_delay: 500ms
|
||||
gc_depth: 50
|
||||
sync_retry_delay: 5000ms
|
||||
sync_retry_nodes: 3
|
||||
batch_size: 500000
|
||||
max_batch_delay: 100ms
|
||||
block_synchronizer:
|
||||
range_synchronize_timeout: 30000ms
|
||||
certificates_synchronize_timeout: 30000ms
|
||||
payload_synchronize_timeout: 30000ms
|
||||
payload_availability_timeout: 30000ms
|
||||
handler_certificate_deliver_timeout: 30000ms
|
||||
consensus_api_grpc:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/39889/http
|
||||
get_collections_timeout: 5000ms
|
||||
remove_collections_timeout: 5000ms
|
||||
max_concurrent_requests: 500000
|
||||
prometheus_metrics:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/40255/http
|
||||
network_admin_server:
|
||||
primary_network_admin_server_port: 38277
|
||||
worker_network_admin_server_base_port: 41135
|
||||
anemo:
|
||||
send_certificate_rate_limit: ~
|
||||
get_payload_availability_rate_limit: ~
|
||||
get_certificates_rate_limit: ~
|
||||
report_batch_rate_limit: ~
|
||||
request_batch_rate_limit: ~
|
||||
enable-event-processing: false
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: 20000000000
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:35253"
|
||||
external-address: /ip4/127.0.0.1/udp/35253
|
||||
genesis:
|
||||
genesis-file-location: /root/.sui/sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 1
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 10
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
|
@ -0,0 +1 @@
|
|||
build
|
|
@ -0,0 +1,15 @@
|
|||
.PHONY: all clean test check
|
||||
|
||||
all: check
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
sui move build -d
|
||||
|
||||
.PHONY: test
|
||||
test: check
|
||||
sui move test -d
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "Coins"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies.Sui]
|
||||
git = "https://github.com/MystenLabs/sui.git"
|
||||
subdir = "crates/sui-framework/packages/sui-framework"
|
||||
rev = "09b2081498366df936abae26eea4b2d5cafb2788"
|
||||
|
||||
[dependencies.Wormhole]
|
||||
local = "../../wormhole"
|
||||
|
||||
[dependencies.TokenBridge]
|
||||
local = "../../token_bridge"
|
||||
|
||||
[addresses]
|
||||
coins = "_"
|
|
@ -0,0 +1,45 @@
|
|||
# @generated by Move, please check-in and do not edit manually.
|
||||
|
||||
[move]
|
||||
version = 0
|
||||
|
||||
dependencies = [
|
||||
{ name = "Sui" },
|
||||
]
|
||||
|
||||
dev-dependencies = [
|
||||
{ name = "TokenBridge" },
|
||||
{ name = "Wormhole" },
|
||||
]
|
||||
|
||||
[[move.package]]
|
||||
name = "MoveStdlib"
|
||||
source = { git = "https://github.com/MystenLabs/sui.git", rev = "09b2081498366df936abae26eea4b2d5cafb2788", subdir = "crates/sui-framework/packages/move-stdlib" }
|
||||
|
||||
[[move.package]]
|
||||
name = "Sui"
|
||||
source = { git = "https://github.com/MystenLabs/sui.git", rev = "09b2081498366df936abae26eea4b2d5cafb2788", subdir = "crates/sui-framework/packages/sui-framework" }
|
||||
|
||||
dependencies = [
|
||||
{ name = "MoveStdlib" },
|
||||
]
|
||||
|
||||
[[move.package]]
|
||||
name = "TokenBridge"
|
||||
source = { local = "../../token_bridge" }
|
||||
|
||||
dependencies = [
|
||||
{ name = "Sui" },
|
||||
]
|
||||
|
||||
dev-dependencies = [
|
||||
{ name = "Wormhole" },
|
||||
]
|
||||
|
||||
[[move.package]]
|
||||
name = "Wormhole"
|
||||
source = { local = "../../wormhole" }
|
||||
|
||||
dependencies = [
|
||||
{ name = "Sui" },
|
||||
]
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "Coins"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies.Sui]
|
||||
git = "https://github.com/MystenLabs/sui.git"
|
||||
subdir = "crates/sui-framework/packages/sui-framework"
|
||||
rev = "09b2081498366df936abae26eea4b2d5cafb2788"
|
||||
|
||||
[dependencies.Wormhole]
|
||||
local = "../../wormhole"
|
||||
|
||||
[dependencies.TokenBridge]
|
||||
local = "../../token_bridge"
|
||||
|
||||
[addresses]
|
||||
coins = "_"
|
||||
|
||||
[dev-dependencies.Wormhole]
|
||||
local = "../../wormhole"
|
||||
|
||||
[dev-dependencies.TokenBridge]
|
||||
local = "../../token_bridge"
|
||||
|
||||
[dev-addresses]
|
||||
wormhole = "0x100"
|
||||
token_bridge = "0x200"
|
||||
coins = "0x20c"
|
|
@ -0,0 +1,210 @@
|
|||
// Example wrapped coin for testing purposes
|
||||
|
||||
#[test_only]
|
||||
module coins::coin {
|
||||
use sui::object::{Self};
|
||||
use sui::package::{Self};
|
||||
use sui::transfer::{Self};
|
||||
use sui::tx_context::{Self, TxContext};
|
||||
|
||||
use token_bridge::create_wrapped::{Self};
|
||||
|
||||
struct COIN has drop {}
|
||||
|
||||
fun init(witness: COIN, ctx: &mut TxContext) {
|
||||
use token_bridge::version_control::{V__0_2_0 as V__CURRENT};
|
||||
|
||||
transfer::public_transfer(
|
||||
create_wrapped::prepare_registration<COIN, V__CURRENT>(
|
||||
witness,
|
||||
// TODO: create a version of this for each decimal to be used
|
||||
8,
|
||||
ctx
|
||||
),
|
||||
tx_context::sender(ctx)
|
||||
);
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
/// NOTE: Even though this module is `#[test_only]`, this method is tagged
|
||||
/// with the same macro as a trick to allow another method within this
|
||||
/// module to call `init` using OTW.
|
||||
public fun init_test_only(ctx: &mut TxContext) {
|
||||
init(COIN {}, ctx);
|
||||
|
||||
// This will be created and sent to the transaction sender
|
||||
// automatically when the contract is published.
|
||||
transfer::public_transfer(
|
||||
package::test_publish(object::id_from_address(@coins), ctx),
|
||||
tx_context::sender(ctx)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module coins::coin_tests {
|
||||
use sui::coin::{Self};
|
||||
use sui::package::{UpgradeCap};
|
||||
use sui::test_scenario::{Self};
|
||||
use token_bridge::create_wrapped::{Self, WrappedAssetSetup};
|
||||
use token_bridge::state::{Self};
|
||||
use token_bridge::token_bridge_scenario::{
|
||||
register_dummy_emitter,
|
||||
return_state,
|
||||
set_up_wormhole_and_token_bridge,
|
||||
take_state,
|
||||
two_people
|
||||
};
|
||||
use token_bridge::token_registry::{Self};
|
||||
use token_bridge::vaa::{Self};
|
||||
use token_bridge::wrapped_asset::{Self};
|
||||
use wormhole::bytes32::{Self};
|
||||
use wormhole::external_address::{Self};
|
||||
use wormhole::wormhole_scenario::{parse_and_verify_vaa};
|
||||
|
||||
use token_bridge::version_control::{V__0_2_0 as V__CURRENT};
|
||||
|
||||
use coins::coin::{COIN};
|
||||
|
||||
// +------------------------------------------------------------------------------+
|
||||
// | Wormhole VAA v1 | nonce: 1 | time: 1 |
|
||||
// | guardian set #0 | #22080291 | consistency: 0 |
|
||||
// |------------------------------------------------------------------------------|
|
||||
// | Signature: |
|
||||
// | #0: 80366065746148420220f25a6275097370e8db40984529a6676b7a5fc9fe... |
|
||||
// |------------------------------------------------------------------------------|
|
||||
// | Emitter: 0x00000000000000000000000000000000deadbeef (Ethereum) |
|
||||
// |==============================================================================|
|
||||
// | Token attestation |
|
||||
// | decimals: 12 |
|
||||
// | Token: 0x00000000000000000000000000000000beefface (Ethereum) |
|
||||
// | Symbol: BEEF |
|
||||
// | Name: Beef face Token |
|
||||
// +------------------------------------------------------------------------------+
|
||||
const VAA: vector<u8> =
|
||||
x"0100000000010080366065746148420220f25a6275097370e8db40984529a6676b7a5fc9feb11755ec49ca626b858ddfde88d15601f85ab7683c5f161413b0412143241c700aff010000000100000001000200000000000000000000000000000000000000000000000000000000deadbeef000000000150eb23000200000000000000000000000000000000000000000000000000000000beefface00020c424545460000000000000000000000000000000000000000000000000000000042656566206661636520546f6b656e0000000000000000000000000000000000";
|
||||
|
||||
// +------------------------------------------------------------------------------+
|
||||
// | Wormhole VAA v1 | nonce: 69 | time: 0 |
|
||||
// | guardian set #0 | #1 | consistency: 15 |
|
||||
// |------------------------------------------------------------------------------|
|
||||
// | Signature: |
|
||||
// | #0: b0571650590e147fce4eb60105e0463522c1244a97bd5dcb365d3e7bc7f3... |
|
||||
// |------------------------------------------------------------------------------|
|
||||
// | Emitter: 0x00000000000000000000000000000000deadbeef (Ethereum) |
|
||||
// |==============================================================================|
|
||||
// | Token attestation |
|
||||
// | decimals: 12 |
|
||||
// | Token: 0x00000000000000000000000000000000beefface (Ethereum) |
|
||||
// | Symbol: BEEF??? and profit |
|
||||
// | Name: Beef face Token??? and profit |
|
||||
// +------------------------------------------------------------------------------+
|
||||
const UPDATED_VAA: vector<u8> =
|
||||
x"0100000000010062f4dcd21bbbc4af8b8baaa2da3a0b168efc4c975de5b828c7a3c710b67a0a0d476d10a74aba7a7867866daf97d1372d8e6ee62ccc5ae522e3e603c67fa23787000000000000000045000200000000000000000000000000000000000000000000000000000000deadbeef00000000000000010f0200000000000000000000000000000000000000000000000000000000beefface00020c424545463f3f3f20616e642070726f666974000000000000000000000000000042656566206661636520546f6b656e3f3f3f20616e642070726f666974000000";
|
||||
|
||||
|
||||
#[test]
|
||||
public fun test_complete_and_update_attestation() {
|
||||
let (caller, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(caller);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Set up contracts.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register foreign emitter on chain ID == 2.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Ignore effects. Make sure `coin_deployer` receives
|
||||
// `WrappedAssetSetup`.
|
||||
test_scenario::next_tx(scenario, coin_deployer);
|
||||
|
||||
// Publish coin.
|
||||
coins::coin::init_test_only(test_scenario::ctx(scenario));
|
||||
|
||||
// Ignore effects.
|
||||
test_scenario::next_tx(scenario, coin_deployer);
|
||||
|
||||
let wrapped_asset_setup =
|
||||
test_scenario::take_from_address<WrappedAssetSetup<COIN, V__CURRENT>>(
|
||||
scenario,
|
||||
coin_deployer
|
||||
);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
let verified_vaa = parse_and_verify_vaa(scenario, VAA);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
let coin_meta = test_scenario::take_shared(scenario);
|
||||
|
||||
// Ignore effects.
|
||||
test_scenario::next_tx(scenario, caller);
|
||||
|
||||
create_wrapped::complete_registration(
|
||||
&mut token_bridge_state,
|
||||
&mut coin_meta,
|
||||
wrapped_asset_setup,
|
||||
test_scenario::take_from_address<UpgradeCap>(
|
||||
scenario,
|
||||
coin_deployer
|
||||
),
|
||||
msg
|
||||
);
|
||||
|
||||
// Check registry.
|
||||
{
|
||||
let verified = state::verified_asset<COIN>(&token_bridge_state);
|
||||
assert!(token_bridge::token_registry::is_wrapped<COIN>(&verified), 0);
|
||||
|
||||
let registry = state::borrow_token_registry(&token_bridge_state);
|
||||
let asset =
|
||||
token_registry::borrow_wrapped<COIN>(registry);
|
||||
assert!(wrapped_asset::total_supply(asset) == 0, 0);
|
||||
|
||||
// Decimals are capped for this wrapped asset.
|
||||
assert!(coin::get_decimals(&coin_meta) == 8, 0);
|
||||
|
||||
// Check metadata against asset metadata.
|
||||
let info = wrapped_asset::info(asset);
|
||||
assert!(wrapped_asset::token_chain(info) == 2, 0);
|
||||
assert!(wrapped_asset::token_address(info) == external_address::new(bytes32::from_bytes(x"00000000000000000000000000000000beefface")), 0);
|
||||
assert!(
|
||||
wrapped_asset::native_decimals(info) == 12,
|
||||
0
|
||||
);
|
||||
assert!(coin::get_symbol(&coin_meta) == std::ascii::string(b"BEEF"), 0);
|
||||
assert!(coin::get_name(&coin_meta) == std::string::utf8(b"Beef face Token"), 0);
|
||||
};
|
||||
|
||||
let verified_vaa =
|
||||
parse_and_verify_vaa(scenario, UPDATED_VAA);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
// Ignore effects.
|
||||
test_scenario::next_tx(scenario, caller);
|
||||
|
||||
// Now update metadata.
|
||||
create_wrapped::update_attestation<COIN>(&mut token_bridge_state, &mut coin_meta, msg);
|
||||
|
||||
// Check updated name and symbol.
|
||||
assert!(
|
||||
coin::get_name(&coin_meta) == std::string::utf8(b"Beef face Token??? and profit"),
|
||||
0
|
||||
);
|
||||
assert!(
|
||||
coin::get_symbol(&coin_meta) == std::ascii::string(b"BEEF??? and profit"),
|
||||
0
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
return_state(token_bridge_state);
|
||||
test_scenario::return_shared(coin_meta);
|
||||
|
||||
|
||||
// Done.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
module coins::coin_10 {
|
||||
use std::option;
|
||||
use sui::coin::{Self, TreasuryCap, CoinMetadata};
|
||||
use sui::transfer;
|
||||
use sui::tx_context::{Self, TxContext};
|
||||
|
||||
/// The type identifier of coin. The coin will have a type
|
||||
/// tag of kind: `Coin<package_object::coin_10::COIN_10>`
|
||||
/// Make sure that the name of the type matches the module's name.
|
||||
struct COIN_10 has drop {}
|
||||
|
||||
/// Module initializer is called once on module publish. A treasury
|
||||
/// cap is sent to the publisher, who then controls minting and burning
|
||||
fun init(witness: COIN_10, ctx: &mut TxContext) {
|
||||
let (treasury, metadata) = create_coin(witness, ctx);
|
||||
transfer::public_freeze_object(metadata);
|
||||
transfer::public_transfer(treasury, tx_context::sender(ctx));
|
||||
}
|
||||
|
||||
fun create_coin(
|
||||
witness: COIN_10,
|
||||
ctx: &mut TxContext
|
||||
): (TreasuryCap<COIN_10>, CoinMetadata<COIN_10>) {
|
||||
coin::create_currency(
|
||||
witness,
|
||||
10, // decimals
|
||||
b"COIN_10", // symbol
|
||||
b"10-Decimal Coin", // name
|
||||
b"", // description
|
||||
option::none(), // icon_url
|
||||
ctx
|
||||
)
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun create_coin_test_only(
|
||||
ctx: &mut TxContext
|
||||
): (TreasuryCap<COIN_10>, CoinMetadata<COIN_10>) {
|
||||
create_coin(COIN_10 {}, ctx)
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun init_test_only(ctx: &mut TxContext) {
|
||||
init(COIN_10 {}, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module coins::coin_10_tests {
|
||||
use sui::test_scenario::{Self};
|
||||
|
||||
use coins::coin_10::{Self};
|
||||
|
||||
#[test]
|
||||
public fun init_test() {
|
||||
let my_scenario = test_scenario::begin(@0x0);
|
||||
let scenario = &mut my_scenario;
|
||||
let creator = @0xDEADBEEF;
|
||||
|
||||
// Proceed.
|
||||
test_scenario::next_tx(scenario, creator);
|
||||
|
||||
// Init.
|
||||
coin_10::init_test_only(test_scenario::ctx(scenario));
|
||||
|
||||
// Proceed.
|
||||
test_scenario::next_tx(scenario, creator);
|
||||
|
||||
// Done.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
module coins::coin_8 {
|
||||
use std::option::{Self};
|
||||
use sui::coin::{Self, TreasuryCap, CoinMetadata};
|
||||
use sui::transfer::{Self};
|
||||
use sui::tx_context::{Self, TxContext};
|
||||
|
||||
/// The type identifier of coin. The coin will have a type
|
||||
/// tag of kind: `Coin<package_object::coin_8::COIN_8>`
|
||||
/// Make sure that the name of the type matches the module's name.
|
||||
struct COIN_8 has drop {}
|
||||
|
||||
/// Module initializer is called once on module publish. A treasury
|
||||
/// cap is sent to the publisher, who then controls minting and burning
|
||||
fun init(witness: COIN_8, ctx: &mut TxContext) {
|
||||
let (treasury, metadata) = create_coin(witness, ctx);
|
||||
transfer::public_freeze_object(metadata);
|
||||
transfer::public_transfer(treasury, tx_context::sender(ctx));
|
||||
}
|
||||
|
||||
fun create_coin(
|
||||
witness: COIN_8,
|
||||
ctx: &mut TxContext
|
||||
): (TreasuryCap<COIN_8>, CoinMetadata<COIN_8>) {
|
||||
coin::create_currency(
|
||||
witness,
|
||||
8, // decimals
|
||||
b"COIN_8", // symbol
|
||||
b"8-Decimal Coin", // name
|
||||
b"", // description
|
||||
option::none(), // icon_url
|
||||
ctx
|
||||
)
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun create_coin_test_only(
|
||||
ctx: &mut TxContext
|
||||
): (TreasuryCap<COIN_8>, CoinMetadata<COIN_8>) {
|
||||
create_coin(COIN_8 {}, ctx)
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun init_test_only(ctx: &mut TxContext) {
|
||||
init(COIN_8 {}, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module coins::coin_8_tests {
|
||||
use sui::test_scenario::{Self};
|
||||
|
||||
use coins::coin_8::{Self};
|
||||
|
||||
#[test]
|
||||
public fun init_test() {
|
||||
let my_scenario = test_scenario::begin(@0x0);
|
||||
let scenario = &mut my_scenario;
|
||||
let creator = @0xDEADBEEF;
|
||||
|
||||
// Proceed.
|
||||
test_scenario::next_tx(scenario, creator);
|
||||
|
||||
// Init.
|
||||
coin_8::init_test_only(test_scenario::ctx(scenario));
|
||||
|
||||
// Proceed.
|
||||
test_scenario::next_tx(scenario, creator);
|
||||
|
||||
// Done.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
-include ../../../Makefile.help
|
||||
|
||||
.PHONY: artifacts
|
||||
artifacts: clean
|
||||
|
||||
.PHONY: clean
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
.PHONY: build
|
||||
# Build contract
|
||||
build:
|
||||
sui move build
|
||||
|
||||
.PHONY: test
|
||||
# Run tests
|
||||
test:
|
||||
sui move build -d || exit $?
|
||||
sui move test -t 1
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "CoreMessages"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies.Sui]
|
||||
git = "https://github.com/MystenLabs/sui.git"
|
||||
subdir = "crates/sui-framework/packages/sui-framework"
|
||||
rev = "09b2081498366df936abae26eea4b2d5cafb2788"
|
||||
|
||||
[dependencies.Wormhole]
|
||||
local = "../../wormhole"
|
||||
|
||||
[addresses]
|
||||
core_messages = "_"
|
|
@ -0,0 +1,32 @@
|
|||
# @generated by Move, please check-in and do not edit manually.
|
||||
|
||||
[move]
|
||||
version = 0
|
||||
|
||||
dependencies = [
|
||||
{ name = "Sui" },
|
||||
]
|
||||
|
||||
dev-dependencies = [
|
||||
{ name = "Wormhole" },
|
||||
]
|
||||
|
||||
[[move.package]]
|
||||
name = "MoveStdlib"
|
||||
source = { git = "https://github.com/MystenLabs/sui.git", rev = "09b2081498366df936abae26eea4b2d5cafb2788", subdir = "crates/sui-framework/packages/move-stdlib" }
|
||||
|
||||
[[move.package]]
|
||||
name = "Sui"
|
||||
source = { git = "https://github.com/MystenLabs/sui.git", rev = "09b2081498366df936abae26eea4b2d5cafb2788", subdir = "crates/sui-framework/packages/sui-framework" }
|
||||
|
||||
dependencies = [
|
||||
{ name = "MoveStdlib" },
|
||||
]
|
||||
|
||||
[[move.package]]
|
||||
name = "Wormhole"
|
||||
source = { local = "../../wormhole" }
|
||||
|
||||
dependencies = [
|
||||
{ name = "Sui" },
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
[package]
|
||||
name = "CoreMessages"
|
||||
version = "1.0.0"
|
||||
|
||||
[dependencies.Sui]
|
||||
git = "https://github.com/MystenLabs/sui.git"
|
||||
subdir = "crates/sui-framework/packages/sui-framework"
|
||||
rev = "09b2081498366df936abae26eea4b2d5cafb2788"
|
||||
|
||||
[dependencies.Wormhole]
|
||||
local = "../../wormhole"
|
||||
|
||||
[addresses]
|
||||
core_messages = "_"
|
||||
|
||||
[dev-dependencies.Wormhole]
|
||||
local = "../../wormhole"
|
||||
|
||||
[dev-addresses]
|
||||
wormhole = "0x100"
|
||||
core_messages = "0x169"
|
|
@ -0,0 +1,149 @@
|
|||
/// A simple contracts that demonstrates how to send messages with wormhole.
|
||||
module core_messages::sender {
|
||||
use sui::clock::{Clock};
|
||||
use sui::coin::{Self};
|
||||
use sui::object::{Self, UID};
|
||||
use sui::transfer::{Self};
|
||||
use sui::tx_context::{TxContext};
|
||||
use wormhole::emitter::{Self, EmitterCap};
|
||||
use wormhole::state::{State as WormholeState};
|
||||
|
||||
struct State has key, store {
|
||||
id: UID,
|
||||
emitter_cap: EmitterCap,
|
||||
}
|
||||
|
||||
/// Register ourselves as a wormhole emitter. This gives back an
|
||||
/// `EmitterCap` which will be required to send messages through
|
||||
/// wormhole.
|
||||
public fun init_with_params(
|
||||
wormhole_state: &WormholeState,
|
||||
ctx: &mut TxContext
|
||||
) {
|
||||
transfer::share_object(
|
||||
State {
|
||||
id: object::new(ctx),
|
||||
emitter_cap: emitter::new(wormhole_state, ctx)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public fun send_message_entry(
|
||||
state: &mut State,
|
||||
wormhole_state: &mut WormholeState,
|
||||
payload: vector<u8>,
|
||||
the_clock: &Clock,
|
||||
ctx: &mut TxContext
|
||||
) {
|
||||
send_message(
|
||||
state,
|
||||
wormhole_state,
|
||||
payload,
|
||||
the_clock,
|
||||
ctx
|
||||
);
|
||||
}
|
||||
|
||||
/// NOTE: This is NOT the proper way of using the `prepare_message` and
|
||||
/// `publish_message` workflow. This example app is meant for testing for
|
||||
/// observing Wormhole messages via the guardian.
|
||||
///
|
||||
/// See `publish_message` module for more info.
|
||||
public fun send_message(
|
||||
state: &mut State,
|
||||
wormhole_state: &mut WormholeState,
|
||||
payload: vector<u8>,
|
||||
the_clock: &Clock,
|
||||
ctx: &mut TxContext
|
||||
): u64 {
|
||||
use wormhole::publish_message::{prepare_message, publish_message};
|
||||
|
||||
// NOTE AGAIN: Integrators should NEVER call this within their contract.
|
||||
publish_message(
|
||||
wormhole_state,
|
||||
coin::zero(ctx),
|
||||
prepare_message(
|
||||
&mut state.emitter_cap,
|
||||
0, // Set nonce to 0, intended for batch VAAs.
|
||||
payload
|
||||
),
|
||||
the_clock
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module core_messages::sender_test {
|
||||
use sui::test_scenario::{Self};
|
||||
use wormhole::wormhole_scenario::{
|
||||
return_clock,
|
||||
return_state,
|
||||
set_up_wormhole,
|
||||
take_clock,
|
||||
take_state,
|
||||
two_people,
|
||||
};
|
||||
|
||||
use core_messages::sender::{
|
||||
State,
|
||||
init_with_params,
|
||||
send_message,
|
||||
};
|
||||
|
||||
#[test]
|
||||
public fun test_send_message() {
|
||||
let (user, admin) = two_people();
|
||||
let my_scenario = test_scenario::begin(admin);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Initialize Wormhole.
|
||||
let wormhole_message_fee = 0;
|
||||
set_up_wormhole(scenario, wormhole_message_fee);
|
||||
|
||||
// Initialize sender module.
|
||||
test_scenario::next_tx(scenario, admin);
|
||||
{
|
||||
let wormhole_state = take_state(scenario);
|
||||
init_with_params(&mut wormhole_state, test_scenario::ctx(scenario));
|
||||
return_state(wormhole_state);
|
||||
};
|
||||
|
||||
// Send message as an ordinary user.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
{
|
||||
let state = test_scenario::take_shared<State>(scenario);
|
||||
let wormhole_state = take_state(scenario);
|
||||
let the_clock = take_clock(scenario);
|
||||
|
||||
let first_message_sequence = send_message(
|
||||
&mut state,
|
||||
&mut wormhole_state,
|
||||
b"Hello",
|
||||
&the_clock,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
assert!(first_message_sequence == 0, 0);
|
||||
|
||||
let second_message_sequence = send_message(
|
||||
&mut state,
|
||||
&mut wormhole_state,
|
||||
b"World",
|
||||
&the_clock,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
assert!(second_message_sequence == 1, 0);
|
||||
|
||||
// Clean up.
|
||||
test_scenario::return_shared(state);
|
||||
return_state(wormhole_state);
|
||||
return_clock(the_clock);
|
||||
};
|
||||
|
||||
// Check effects.
|
||||
let effects = test_scenario::next_tx(scenario, user);
|
||||
assert!(test_scenario::num_user_events(&effects) == 2, 0);
|
||||
|
||||
// End test.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# Templates
|
||||
|
||||
This directory contains templates for Sui contracts. These templates aren't fully functional contracts and require substitution of variables prior to deployment. For example, the `wrapped_coin` template requires the version control struct name as well as the decimals of the wrapped token.
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "WrappedCoin"
|
||||
version = "0.0.1"
|
||||
|
||||
[dependencies.Sui]
|
||||
git = "https://github.com/MystenLabs/sui.git"
|
||||
subdir = "crates/sui-framework/packages/sui-framework"
|
||||
rev = "09b2081498366df936abae26eea4b2d5cafb2788"
|
||||
|
||||
[dependencies.Wormhole]
|
||||
local = "../../wormhole"
|
||||
|
||||
[dependencies.TokenBridge]
|
||||
local = "../../token_bridge"
|
||||
|
||||
[addresses]
|
||||
wormhole = "_"
|
||||
token_bridge = "_"
|
||||
wrapped_coin = "0x0"
|
|
@ -0,0 +1,21 @@
|
|||
module wrapped_coin::coin {
|
||||
use sui::transfer::{Self};
|
||||
use sui::tx_context::{Self, TxContext};
|
||||
|
||||
use token_bridge::create_wrapped::{Self};
|
||||
|
||||
struct COIN has drop {}
|
||||
|
||||
fun init(witness: COIN, ctx: &mut TxContext) {
|
||||
use token_bridge::version_control::{{{VERSION}}};
|
||||
|
||||
transfer::public_transfer(
|
||||
create_wrapped::prepare_registration<COIN, {{VERSION}}>(
|
||||
witness,
|
||||
{{DECIMALS}},
|
||||
ctx
|
||||
),
|
||||
tx_context::sender(ctx)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
#!/bin/bash -f
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
. ../env.sh
|
||||
|
||||
echo "Creating wrapped asset..."
|
||||
echo "$COIN_PACKAGE::coin_witness::COIN_WITNESS"
|
||||
|
||||
sui client call --function register_wrapped_coin \
|
||||
--module wrapped --package $TOKEN_PACKAGE --gas-budget 20000 \
|
||||
--args "$WORM_STATE" "$TOKEN_STATE" "$NEW_WRAPPED_COIN" \
|
||||
--type-args "$COIN_PACKAGE::coin_witness::COIN_WITNESS"
|
|
@ -1,54 +1,119 @@
|
|||
#!/bin/bash -f
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
# Help message
|
||||
function usage() {
|
||||
cat <<EOF >&2
|
||||
Deploy and initialize Sui core bridge and token bridge contracts to the
|
||||
specified network. Additionally deploys an example messaging contract in
|
||||
devnet.
|
||||
|
||||
#Transaction Kind : Publish
|
||||
#----- Transaction Effects ----
|
||||
#Status : Success
|
||||
#Created Objects:
|
||||
# - ID: 0x069b6d8ea50a0b0756518cb08ddbbad2babf8ae0 <= STATE , Owner: Account Address ( 0xe6a09658743da40b0f48c4da1f3fa0d34797d0d3 <= OWNER )
|
||||
# - ID: 0x73fc05ae6f172f90b12a98cf3ad0b669d6b70e5b <= PACKAGE , Owner: Immutable
|
||||
Usage: $(basename "$0") <network> [options]
|
||||
|
||||
cd wormhole
|
||||
sed -i -e 's/wormhole = .*/wormhole = "0x0"/' Move.toml
|
||||
make build
|
||||
sui client publish --gas-budget 10000 | tee publish.log
|
||||
grep ID: publish.log | head -2 > ids.log
|
||||
Positional args:
|
||||
<network> Network to deploy to (devnet, testnet, mainnet)
|
||||
|
||||
WORM_PACKAGE=$(grep "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
|
||||
sed -i -e "s/wormhole = .*/wormhole = \"$WORM_PACKAGE\"/" Move.toml
|
||||
WORM_DEPLOYER_CAPABILITY=$(grep -v "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
|
||||
WORM_OWNER=$(grep -v "Immutable" ids.log | sed -e 's/^.*( \(.*\) )/\1/')
|
||||
Options:
|
||||
-k, --private-key Use given key to sign transactions
|
||||
-h, --help Show this help message
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
cd ../token_bridge
|
||||
sed -i -e 's/token_bridge = .*/token_bridge = "0x0"/' Move.toml
|
||||
make build
|
||||
sui client publish --gas-budget 10000 | tee publish.log
|
||||
grep ID: publish.log | head -2 > ids.log
|
||||
# If positional args are missing, print help message and exit
|
||||
if [ $# -lt 1 ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
TOKEN_PACKAGE=$(grep "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
|
||||
sed -i -e "s/token_bridge = .*/token_bridge = \"$TOKEN_PACKAGE\"/" Move.toml
|
||||
TOKEN_DEPLOYER_CAPABILITY=$(grep -v "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
|
||||
TOKEN_OWNER=$(grep -v "Immutable" ids.log | sed -e 's/^.*( \(.*\) )/\1/')
|
||||
# Default values
|
||||
PRIVATE_KEY_ARG=
|
||||
|
||||
sui client call --function init_and_share_state --module state --package $WORM_PACKAGE --gas-budget 20000 --args \"$WORM_DEPLOYER_CAPABILITY\" 0 0 "[190,250,66,157,87,205,24,183,248,164,217,26,45,169,171,74,240,93,15,190]" "[[190,250,66,157,87,205,24,183,248,164,217,26,45,169,171,74,240,93,15,190]]" | tee wormhole.log
|
||||
WORM_STATE=$(grep Shared wormhole.log | head -1 | sed -e 's/^.*: \(.*\) ,.*/\1/')
|
||||
# Set network
|
||||
NETWORK=$1 || usage
|
||||
shift
|
||||
|
||||
sui client call --function get_new_emitter --module wormhole --package $WORM_PACKAGE --gas-budget 20000 --args \"$WORM_STATE\" | tee emitter.log
|
||||
TOKEN_EMITTER_CAPABILITY=$(grep ID: emitter.log | head -1 | sed -e 's/^.*: \(.*\) ,.*/\1/')
|
||||
# Set guardian address
|
||||
if [ "$NETWORK" = mainnet ]; then
|
||||
echo "Mainnet not supported yet"
|
||||
exit 1
|
||||
elif [ "$NETWORK" = testnet ]; then
|
||||
echo "Testnet not supported yet"
|
||||
exit 1
|
||||
elif [ "$NETWORK" = devnet ]; then
|
||||
GUARDIAN_ADDR=befa429d57cd18b7f8a4d91a2da9ab4af05d0fbe
|
||||
else
|
||||
usage
|
||||
fi
|
||||
|
||||
sui client call --function init_and_share_state --module bridge_state --package $TOKEN_PACKAGE --gas-budget 20000 --args "$TOKEN_DEPLOYER_CAPABILITY" "$TOKEN_EMITTER_CAPABILITY" | tee token.log
|
||||
TOKEN_STATE=$(grep Shared token.log | head -1 | sed -e 's/^.*: \(.*\) ,.*/\1/')
|
||||
# Parse short/long flags
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-k|--private-key)
|
||||
if [[ ! -z "$2" ]]; then
|
||||
PRIVATE_KEY_ARG="-k $2"
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
{ echo "export WORM_PACKAGE=$WORM_PACKAGE";
|
||||
echo "export WORM_DEPLOYER_CAPABILITY=$WORM_DEPLOYER_CAPABILITY";
|
||||
echo "export WORM_OWNER=$WORM_OWNER";
|
||||
echo "export TOKEN_PACKAGE=$TOKEN_PACKAGE";
|
||||
echo "export TOKEN_DEPLOYER_CAPABILITY=$TOKEN_DEPLOYER_CAPABILITY";
|
||||
echo "export TOKEN_OWNER=$TOKEN_OWNER";
|
||||
echo "export WORM_STATE=$WORM_STATE";
|
||||
echo "export TOKEN_EMITTER_CAPABILITY=$TOKEN_EMITTER_CAPABILITY";
|
||||
echo "export TOKEN_STATE=$TOKEN_STATE";
|
||||
} > ../env.sh
|
||||
# Assumes this script is in a sibling directory to contract dirs
|
||||
DIRNAME=$(dirname "$0")
|
||||
WORMHOLE_PATH=$(realpath "$DIRNAME"/../wormhole)
|
||||
TOKEN_BRIDGE_PATH=$(realpath "$DIRNAME"/../token_bridge)
|
||||
EXAMPLE_APP_PATH=$(realpath "$DIRNAME"/../examples/core_messages)
|
||||
EXAMPLE_COIN_PATH=$(realpath "$DIRNAME"/../examples/coins)
|
||||
|
||||
echo -e "[1/4] Publishing core bridge contracts..."
|
||||
WORMHOLE_PUBLISH_OUTPUT=$($(echo worm sui deploy "$WORMHOLE_PATH" -n "$NETWORK" "$PRIVATE_KEY_ARG"))
|
||||
echo "$WORMHOLE_PUBLISH_OUTPUT"
|
||||
|
||||
echo -e "\n[2/4] Initializing core bridge..."
|
||||
WORMHOLE_PACKAGE_ID=$(echo "$WORMHOLE_PUBLISH_OUTPUT" | grep -oP 'Published to +\K.*')
|
||||
WORMHOLE_INIT_OUTPUT=$($(echo worm sui init-wormhole -n "$NETWORK" --initial-guardian "$GUARDIAN_ADDR" -p "$WORMHOLE_PACKAGE_ID" "$PRIVATE_KEY_ARG"))
|
||||
WORMHOLE_STATE_OBJECT_ID=$(echo "$WORMHOLE_INIT_OUTPUT" | grep -oP 'Wormhole state object ID +\K.*')
|
||||
echo "$WORMHOLE_INIT_OUTPUT"
|
||||
|
||||
echo -e "\n[3/4] Publishing token bridge contracts..."
|
||||
TOKEN_BRIDGE_PUBLISH_OUTPUT=$($(echo worm sui deploy "$TOKEN_BRIDGE_PATH" -n "$NETWORK" "$PRIVATE_KEY_ARG"))
|
||||
echo "$TOKEN_BRIDGE_PUBLISH_OUTPUT"
|
||||
|
||||
echo -e "\n[4/4] Initializing token bridge..."
|
||||
TOKEN_BRIDGE_PACKAGE_ID=$(echo "$TOKEN_BRIDGE_PUBLISH_OUTPUT" | grep -oP 'Published to +\K.*')
|
||||
TOKEN_BRIDGE_INIT_OUTPUT=$($(echo worm sui init-token-bridge -n "$NETWORK" -p "$TOKEN_BRIDGE_PACKAGE_ID" -w "$WORMHOLE_STATE_OBJECT_ID" "$PRIVATE_KEY_ARG"))
|
||||
TOKEN_BRIDGE_STATE_OBJECT_ID=$(echo "$TOKEN_BRIDGE_INIT_OUTPUT" | grep -oP 'Token bridge state object ID +\K.*')
|
||||
echo "$TOKEN_BRIDGE_INIT_OUTPUT"
|
||||
|
||||
if [ "$NETWORK" = devnet ]; then
|
||||
echo -e "\n[+1/2] Deploying and initializing example app..."
|
||||
EXAMPLE_APP_PUBLISH_OUTPUT=$($(echo worm sui deploy "$EXAMPLE_APP_PATH" -n "$NETWORK" "$PRIVATE_KEY_ARG"))
|
||||
EXAMPLE_APP_PACKAGE_ID=$(echo "$EXAMPLE_APP_PUBLISH_OUTPUT" | grep -oP 'Published to +\K.*')
|
||||
echo "$EXAMPLE_APP_PUBLISH_OUTPUT"
|
||||
|
||||
EXAMPLE_INIT_OUTPUT=$($(echo worm sui init-example-message-app -n "$NETWORK" -p "$EXAMPLE_APP_PACKAGE_ID" -w "$WORMHOLE_STATE_OBJECT_ID" "$PRIVATE_KEY_ARG"))
|
||||
EXAMPLE_APP_STATE_OBJECT_ID=$(echo "$EXAMPLE_INIT_OUTPUT" | grep -oP 'Example app state object ID +\K.*')
|
||||
echo "$EXAMPLE_INIT_OUTPUT"
|
||||
|
||||
echo -e "\n[+2/2] Deploying example coins..."
|
||||
EXAMPLE_COIN_PUBLISH_OUTPUT=$($(echo worm sui deploy "$EXAMPLE_COIN_PATH" -n "$NETWORK" "$PRIVATE_KEY_ARG"))
|
||||
echo "$EXAMPLE_COIN_PUBLISH_OUTPUT"
|
||||
|
||||
echo -e "\nWormhole package ID: $WORMHOLE_PACKAGE_ID"
|
||||
echo "Token bridge package ID: $TOKEN_BRIDGE_PACKAGE_ID"
|
||||
echo "Wormhole state object ID: $WORMHOLE_STATE_OBJECT_ID"
|
||||
echo "Token bridge state object ID: $TOKEN_BRIDGE_STATE_OBJECT_ID"
|
||||
|
||||
echo -e "\nPublish message command:" worm sui publish-example-message -n devnet -p "$EXAMPLE_APP_PACKAGE_ID" -s "$EXAMPLE_APP_STATE_OBJECT_ID" -w "$WORMHOLE_STATE_OBJECT_ID" -m "hello" "$PRIVATE_KEY_ARG"
|
||||
fi
|
||||
|
||||
echo -e "\nDeployments successful!"
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
#!/bin/bash -f
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"/..
|
||||
|
||||
. env.sh
|
||||
|
||||
sui client publish --gas-budget 20000 --path coin | tee publish.log
|
||||
grep ID: publish.log | head -2 > ids.log
|
||||
NEW_WRAPPED_COIN=$(grep "Account Address" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
|
||||
COIN_PACKAGE=$(grep "Immutable" ids.log | sed -e 's/^.*: \(.*\) ,.*/\1/')
|
||||
echo "export NEW_WRAPPED_COIN=$NEW_WRAPPED_COIN" >> env.sh
|
||||
echo "export COIN_PACKAGE=$COIN_PACKAGE" >> env.sh
|
|
@ -1,2 +0,0 @@
|
|||
#
|
||||
curl -X POST -d '{"FixedAmountRequest":{"recipient": "'"$1"'"}}' -H 'Content-Type: application/json' http://127.0.0.1:5003/gas
|
|
@ -1,3 +0,0 @@
|
|||
#!/bin/bash -f
|
||||
|
||||
sui client transfer-sui --to 0x2acab6bb0e4722e528291bc6ca4f097e18ce9331 --sui-coin-object-id `sui client objects | grep sui::SUI | tail -1 | sed -e 's/|.*//'` --gas-budget 10000
|
|
@ -1,10 +0,0 @@
|
|||
import { Ed25519Keypair, JsonRpcProvider, RawSigner } from '@mysten/sui.js';
|
||||
// Generate a new Secp256k1 Keypair
|
||||
const keypair = new Ed25519Keypair();
|
||||
|
||||
const signer = new RawSigner(
|
||||
keypair,
|
||||
new JsonRpcProvider('https://gateway.devnet.sui.io:443')
|
||||
);
|
||||
|
||||
console.log(keypair)
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash -f
|
||||
|
||||
. env.sh
|
||||
|
||||
sui client call --function get_new_emitter --module wormhole --package $WORM_PACKAGE --gas-budget 20000 --args \"$WORM_STATE\"
|
|
@ -1,24 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# This dev script imports and funds an account, following the steps in
|
||||
# `sui/NOTES.md`. It also deploys the core/token bridge contracts.
|
||||
|
||||
# Remove directory for idempotency
|
||||
rm -rf $HOME/.sui
|
||||
|
||||
# Import key so we have a deterministic address and make it the default account
|
||||
sui keytool import "daughter exclude wheat pudding police weapon giggle taste space whip satoshi occur" ed25519
|
||||
sui client << EOF
|
||||
y
|
||||
http://localhost:9000
|
||||
dev
|
||||
0
|
||||
EOF
|
||||
sed -i -e 's/active_address.*/active_address: "0x13b3cb89cf3226d3b860294fc75dc6c91f0c5ecf"/' ~/.sui/sui_config/client.yaml
|
||||
|
||||
# Fund account
|
||||
kubectl exec -it sui-0 -c sui-node -- /tmp/funder.sh
|
||||
|
||||
# Deploy contracts
|
||||
DIR_PATH="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )"
|
||||
$DIR_PATH/deploy.sh
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash -f
|
||||
|
||||
. env.sh
|
||||
|
||||
sui client call --function init_and_share_state --module bridge_state --package $TOKEN_PACKAGE --gas-budget 20000 --args \"$TOKEN_STATE\" \"$EMITTER_CAP\"
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash -f
|
||||
|
||||
. env.sh
|
||||
|
||||
sui client call --function init_and_share_state --module state --package $WORM_PACKAGE --gas-budget 20000 --args \"$WORM_STATE\" 0 0 [190,250,66,157,87,205,24,183,248,164,217,26,45,169,171,74,240,93,15,190] [190,250,66,157,87,205,24,183,248,164,217,26,45,169,171,74,240,93,15,190]
|
|
@ -4,6 +4,8 @@ source $HOME/.cargo/env
|
|||
|
||||
git clone https://github.com/MystenLabs/sui.git --branch devnet
|
||||
cd sui
|
||||
# Corresponds to https://github.com/MystenLabs/sui/releases/tag/testnet-1.0.0
|
||||
git reset --hard 09b2081498366df936abae26eea4b2d5cafb2788
|
||||
|
||||
cargo --locked install --path crates/sui
|
||||
cargo --locked install --path crates/sui-faucet
|
||||
|
|
|
@ -1,804 +0,0 @@
|
|||
{
|
||||
"name": "sui-scripts",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sui-scripts",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mysten/sui.js": "^0.10.0",
|
||||
"axios": "^1.0.0",
|
||||
"node-fetch": "^3.2.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@mysten/sui.js": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.10.0.tgz",
|
||||
"integrity": "sha512-z9K34+jQBzeUCTcroTExBzYPCNHohyuz1sR85HYkbymDqdRCTi1IcfBzZDinWibZlk0sZhJtjnozxHklsPvYLQ==",
|
||||
"dependencies": {
|
||||
"bn.js": "^5.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"jayson": "^3.6.6",
|
||||
"js-sha3": "^0.8.0",
|
||||
"lossless-json": "^1.0.5",
|
||||
"tweetnacl": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
|
||||
"integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.0.0.tgz",
|
||||
"integrity": "sha512-SsHsGFN1qNPFT5QhSoSD37SHDfGyLSW5AESmyLk2JeCMHv5g0I9g0Hz/zQHx2KNe0jGXh2q2hAm7OdkXm360CA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/bn.js": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
||||
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"node_modules/cross-fetch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
|
||||
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
|
||||
"dependencies": {
|
||||
"node-fetch": "2.6.7"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-fetch/node_modules/node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/data-uri-to-buffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA==",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/delay": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
|
||||
"integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"node_modules/es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
|
||||
"dependencies": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/eyes": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
|
||||
"integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==",
|
||||
"engines": {
|
||||
"node": "> 0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "paypal",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20 || >= 14.13"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"dependencies": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/isomorphic-ws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
|
||||
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
|
||||
"peerDependencies": {
|
||||
"ws": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/jayson": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jayson/-/jayson-3.7.0.tgz",
|
||||
"integrity": "sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==",
|
||||
"dependencies": {
|
||||
"@types/connect": "^3.4.33",
|
||||
"@types/node": "^12.12.54",
|
||||
"@types/ws": "^7.4.4",
|
||||
"commander": "^2.20.3",
|
||||
"delay": "^5.0.0",
|
||||
"es6-promisify": "^5.0.0",
|
||||
"eyes": "^0.1.8",
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"JSONStream": "^1.3.5",
|
||||
"lodash": "^4.17.20",
|
||||
"uuid": "^8.3.2",
|
||||
"ws": "^7.4.5"
|
||||
},
|
||||
"bin": {
|
||||
"jayson": "bin/jayson.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/js-sha3": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
|
||||
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
|
||||
},
|
||||
"node_modules/json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
|
||||
},
|
||||
"node_modules/jsonparse": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
|
||||
"integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
|
||||
"engines": [
|
||||
"node >= 0.2.0"
|
||||
]
|
||||
},
|
||||
"node_modules/JSONStream": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
|
||||
"integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
|
||||
"dependencies": {
|
||||
"jsonparse": "^1.2.0",
|
||||
"through": ">=2.2.7 <3"
|
||||
},
|
||||
"bin": {
|
||||
"JSONStream": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lossless-json": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-1.0.5.tgz",
|
||||
"integrity": "sha512-RicKUuLwZVNZ6ZdJHgIZnSeA05p8qWc5NW0uR96mpPIjN9WDLUg9+kj1esQU1GkPn9iLZVKatSQK5gyiaFHgJA=="
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/jimmywarting"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://paypal.me/jimmywarting"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=10.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-fetch": {
|
||||
"version": "3.2.10",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz",
|
||||
"integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==",
|
||||
"dependencies": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/web-streams-polyfill": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
|
||||
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": "^5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"@mysten/sui.js": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@mysten/sui.js/-/sui.js-0.10.0.tgz",
|
||||
"integrity": "sha512-z9K34+jQBzeUCTcroTExBzYPCNHohyuz1sR85HYkbymDqdRCTi1IcfBzZDinWibZlk0sZhJtjnozxHklsPvYLQ==",
|
||||
"requires": {
|
||||
"bn.js": "^5.2.0",
|
||||
"buffer": "^6.0.3",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"jayson": "^3.6.6",
|
||||
"js-sha3": "^0.8.0",
|
||||
"lossless-json": "^1.0.5",
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"@types/connect": {
|
||||
"version": "3.4.35",
|
||||
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz",
|
||||
"integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "12.20.55",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.55.tgz",
|
||||
"integrity": "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="
|
||||
},
|
||||
"@types/ws": {
|
||||
"version": "7.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz",
|
||||
"integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.0.0.tgz",
|
||||
"integrity": "sha512-SsHsGFN1qNPFT5QhSoSD37SHDfGyLSW5AESmyLk2JeCMHv5g0I9g0Hz/zQHx2KNe0jGXh2q2hAm7OdkXm360CA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"bn.js": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz",
|
||||
"integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="
|
||||
},
|
||||
"buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"requires": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||
},
|
||||
"cross-fetch": {
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz",
|
||||
"integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==",
|
||||
"requires": {
|
||||
"node-fetch": "2.6.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"node-fetch": {
|
||||
"version": "2.6.7",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz",
|
||||
"integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==",
|
||||
"requires": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"data-uri-to-buffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Vr3mLBA8qWmcuschSLAOogKgQ/Jwxulv3RNE4FXnYWRGujzrRWQI4m12fQqRkwX06C0KanhLr4hK+GydchZsaA=="
|
||||
},
|
||||
"delay": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz",
|
||||
"integrity": "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||
},
|
||||
"es6-promise": {
|
||||
"version": "4.2.8",
|
||||
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
|
||||
},
|
||||
"es6-promisify": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
|
||||
"integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
|
||||
"requires": {
|
||||
"es6-promise": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"eyes": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz",
|
||||
"integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="
|
||||
},
|
||||
"fetch-blob": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
||||
"requires": {
|
||||
"node-domexception": "^1.0.0",
|
||||
"web-streams-polyfill": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"follow-redirects": {
|
||||
"version": "1.15.2",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
||||
"requires": {
|
||||
"fetch-blob": "^3.1.2"
|
||||
}
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"isomorphic-ws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz",
|
||||
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
|
||||
"requires": {}
|
||||
},
|
||||
"jayson": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/jayson/-/jayson-3.7.0.tgz",
|
||||
"integrity": "sha512-tfy39KJMrrXJ+mFcMpxwBvFDetS8LAID93+rycFglIQM4kl3uNR3W4lBLE/FFhsoUCEox5Dt2adVpDm/XtebbQ==",
|
||||
"requires": {
|
||||
"@types/connect": "^3.4.33",
|
||||
"@types/node": "^12.12.54",
|
||||
"@types/ws": "^7.4.4",
|
||||
"commander": "^2.20.3",
|
||||
"delay": "^5.0.0",
|
||||
"es6-promisify": "^5.0.0",
|
||||
"eyes": "^0.1.8",
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"JSONStream": "^1.3.5",
|
||||
"lodash": "^4.17.20",
|
||||
"uuid": "^8.3.2",
|
||||
"ws": "^7.4.5"
|
||||
}
|
||||
},
|
||||
"js-sha3": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
|
||||
"integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
|
||||
},
|
||||
"json-stringify-safe": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
|
||||
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
|
||||
},
|
||||
"jsonparse": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
|
||||
"integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="
|
||||
},
|
||||
"JSONStream": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz",
|
||||
"integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==",
|
||||
"requires": {
|
||||
"jsonparse": "^1.2.0",
|
||||
"through": ">=2.2.7 <3"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lossless-json": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/lossless-json/-/lossless-json-1.0.5.tgz",
|
||||
"integrity": "sha512-RicKUuLwZVNZ6ZdJHgIZnSeA05p8qWc5NW0uR96mpPIjN9WDLUg9+kj1esQU1GkPn9iLZVKatSQK5gyiaFHgJA=="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"requires": {
|
||||
"mime-db": "1.52.0"
|
||||
}
|
||||
},
|
||||
"node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "3.2.10",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.2.10.tgz",
|
||||
"integrity": "sha512-MhuzNwdURnZ1Cp4XTazr69K0BTizsBroX7Zx3UgDSVcZYKF/6p0CBe4EUb/hLqmzVhl0UpYfgRljQ4yxE+iCxA==",
|
||||
"requires": {
|
||||
"data-uri-to-buffer": "^4.0.0",
|
||||
"fetch-blob": "^3.1.4",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
}
|
||||
},
|
||||
"proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"through": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
|
||||
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
|
||||
},
|
||||
"tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
|
||||
},
|
||||
"tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
},
|
||||
"web-streams-polyfill": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz",
|
||||
"integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q=="
|
||||
},
|
||||
"webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
|
||||
},
|
||||
"whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"requires": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "7.5.9",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
|
||||
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
|
||||
"requires": {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
{
|
||||
"name": "@wormhole-foundation/sui-scripts",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@mysten/sui.js": "^0.10.0",
|
||||
"axios": "^1.0.0",
|
||||
"node-fetch": "^3.2.10"
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
#!/bin/bash -f
|
||||
|
||||
. env.sh
|
||||
|
||||
sui client call --function publish_message_free --module wormhole --package $WORM_PACKAGE --gas-budget 20000 --args \"$1\" \"$WORM_STATE\" 400 [2]
|
|
@ -0,0 +1,22 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
DOTENV=$(realpath "$(dirname "$0")"/../.env)
|
||||
[ -f $DOTENV ] || (echo "$DOTENV does not exist." >&2; exit 1)
|
||||
|
||||
# 1. load variables from .env file
|
||||
. $DOTENV
|
||||
|
||||
# 2. next we get all the token bridge registration VAAs from the environment
|
||||
# if a new VAA is added, this will automatically pick it up
|
||||
VAAS=$(set | grep "REGISTER_.*_TOKEN_BRIDGE_VAA" | grep -v SUI | cut -d '=' -f1)
|
||||
|
||||
# 3. use 'worm' to submit each registration VAA
|
||||
for VAA in $VAAS
|
||||
do
|
||||
VAA=${!VAA}
|
||||
worm submit $VAA --chain sui --network devnet
|
||||
done
|
||||
|
||||
echo "Registrations successful."
|
|
@ -2,14 +2,6 @@
|
|||
|
||||
set -x
|
||||
|
||||
sui start &
|
||||
sleep 10
|
||||
#sleep infinity
|
||||
sui client object --id 0x5
|
||||
#sui-faucet --host-ip 0.0.0.0&
|
||||
#sleep 2
|
||||
#curl -X POST -d '{"FixedAmountRequest":{"recipient": "'"0x2acab6bb0e4722e528291bc6ca4f097e18ce9331"'"}}' -H 'Content-Type: application/json' http://127.0.0.1:5003/gas
|
||||
sed -i -e 's/:9000/:9002/' ~/.sui/sui_config/fullnode.yaml
|
||||
sui-node --config-path ~/.sui/sui_config/fullnode.yaml
|
||||
|
||||
#sleep infinity
|
||||
sui start >/dev/null 2>&1 &
|
||||
sleep 5
|
||||
sui-faucet --write-ahead-log faucet.log
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
# Wait for sui to start
|
||||
while [[ "$(curl -X POST -H "Content-Type: application/json" -d '{ "jsonrpc":"2.0", "method":"rpc.discover","id":1 }' -s -o /dev/null -w '%{http_code}' 0.0.0.0:9000/)" != "200" ]]; do sleep 5; done
|
||||
|
||||
# Wait for sui-faucet to start
|
||||
while [[ "$(curl -s -o /dev/null -w '%{http_code}' 0.0.0.0:5003/)" != "200" ]]; do sleep 5; done
|
|
@ -0,0 +1,4 @@
|
|||
node_modules
|
||||
sui.log.*
|
||||
./token_bridge/
|
||||
./wormhole/
|
|
@ -0,0 +1,13 @@
|
|||
-include ../Makefile.help
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf node_modules
|
||||
|
||||
node_modules:
|
||||
npm ci
|
||||
|
||||
.PHONY: test
|
||||
## Run tests
|
||||
test: node_modules
|
||||
bash run_integration_test.sh
|
|
@ -0,0 +1,78 @@
|
|||
import { expect } from "chai";
|
||||
import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock";
|
||||
|
||||
import {
|
||||
CREATOR_PRIVATE_KEY,
|
||||
GUARDIAN_PRIVATE_KEY,
|
||||
RELAYER_PRIVATE_KEY,
|
||||
WALLET_PRIVATE_KEY,
|
||||
} from "./helpers/consts";
|
||||
import {
|
||||
Ed25519Keypair,
|
||||
JsonRpcProvider,
|
||||
localnetConnection,
|
||||
RawSigner,
|
||||
} from "@mysten/sui.js";
|
||||
|
||||
describe(" 0. Environment", () => {
|
||||
const provider = new JsonRpcProvider(localnetConnection);
|
||||
|
||||
// User wallet.
|
||||
const wallet = new RawSigner(
|
||||
Ed25519Keypair.fromSecretKey(WALLET_PRIVATE_KEY),
|
||||
provider
|
||||
);
|
||||
|
||||
// Relayer wallet.
|
||||
const relayer = new RawSigner(
|
||||
Ed25519Keypair.fromSecretKey(RELAYER_PRIVATE_KEY),
|
||||
provider
|
||||
);
|
||||
|
||||
// Deployer wallet.
|
||||
const creator = new RawSigner(
|
||||
Ed25519Keypair.fromSecretKey(CREATOR_PRIVATE_KEY),
|
||||
provider
|
||||
);
|
||||
|
||||
describe("Verify Local Validator", () => {
|
||||
it("Balance", async () => {
|
||||
// Balance check wallet.
|
||||
{
|
||||
const coinData = await wallet
|
||||
.getAddress()
|
||||
.then((owner) =>
|
||||
provider
|
||||
.getCoins({ owner, coinType: "0x2::sui::SUI" })
|
||||
.then((result) => result.data)
|
||||
);
|
||||
expect(coinData).has.length(5);
|
||||
}
|
||||
|
||||
// Balance check relayer.
|
||||
{
|
||||
const coinData = await relayer
|
||||
.getAddress()
|
||||
.then((owner) =>
|
||||
provider
|
||||
.getCoins({ owner, coinType: "0x2::sui::SUI" })
|
||||
.then((result) => result.data)
|
||||
);
|
||||
expect(coinData).has.length(5);
|
||||
}
|
||||
|
||||
// Balance check creator. This should only have one gas object at this
|
||||
// point.
|
||||
{
|
||||
const coinData = await creator
|
||||
.getAddress()
|
||||
.then((owner) =>
|
||||
provider
|
||||
.getCoins({ owner, coinType: "0x2::sui::SUI" })
|
||||
.then((result) => result.data)
|
||||
);
|
||||
expect(coinData).has.length(1);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,109 @@
|
|||
import { expect } from "chai";
|
||||
|
||||
import { WALLET_PRIVATE_KEY, WORMHOLE_STATE_ID } from "./helpers/consts";
|
||||
import {
|
||||
Ed25519Keypair,
|
||||
JsonRpcProvider,
|
||||
localnetConnection,
|
||||
RawSigner,
|
||||
SUI_CLOCK_OBJECT_ID,
|
||||
TransactionBlock,
|
||||
} from "@mysten/sui.js";
|
||||
import { getPackageId } from "./helpers/utils";
|
||||
import { addPrepareMessageAndPublishMessage } from "./helpers/wormhole/testPublishMessage";
|
||||
|
||||
describe(" 1. Wormhole", () => {
|
||||
const provider = new JsonRpcProvider(localnetConnection);
|
||||
|
||||
// User wallet.
|
||||
const wallet = new RawSigner(
|
||||
Ed25519Keypair.fromSecretKey(WALLET_PRIVATE_KEY),
|
||||
provider
|
||||
);
|
||||
|
||||
describe("Publish Message", () => {
|
||||
it("Check `WormholeMessage` Event", async () => {
|
||||
const wormholePackage = await getPackageId(
|
||||
wallet.provider,
|
||||
WORMHOLE_STATE_ID
|
||||
);
|
||||
|
||||
const owner = await wallet.getAddress();
|
||||
|
||||
// Create emitter cap.
|
||||
const emitterCapId = await (async () => {
|
||||
const tx = new TransactionBlock();
|
||||
const [emitterCap] = tx.moveCall({
|
||||
target: `${wormholePackage}::emitter::new`,
|
||||
arguments: [tx.object(WORMHOLE_STATE_ID)],
|
||||
});
|
||||
tx.transferObjects([emitterCap], tx.pure(owner));
|
||||
|
||||
// Execute and fetch created Emitter cap.
|
||||
return wallet
|
||||
.signAndExecuteTransactionBlock({
|
||||
transactionBlock: tx,
|
||||
options: {
|
||||
showObjectChanges: true,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
const found = result.objectChanges?.filter(
|
||||
(item) => "created" === item.type!
|
||||
);
|
||||
if (found?.length == 1 && "objectId" in found[0]) {
|
||||
return found[0].objectId;
|
||||
}
|
||||
|
||||
throw new Error("no objects found");
|
||||
});
|
||||
})();
|
||||
|
||||
// Publish messages using emitter cap.
|
||||
{
|
||||
const nonce = 69;
|
||||
const basePayload = "All your base are belong to us.";
|
||||
|
||||
const numMessages = 32;
|
||||
const payloads: string[] = [];
|
||||
const tx = new TransactionBlock();
|
||||
|
||||
// Construct transaction block to send multiple messages.
|
||||
for (let i = 0; i < numMessages; ++i) {
|
||||
// Make a unique message.
|
||||
const payload = basePayload + `... ${i}`;
|
||||
payloads.push(payload);
|
||||
|
||||
addPrepareMessageAndPublishMessage(
|
||||
tx,
|
||||
wormholePackage,
|
||||
WORMHOLE_STATE_ID,
|
||||
emitterCapId,
|
||||
nonce,
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
const events = await wallet
|
||||
.signAndExecuteTransactionBlock({
|
||||
transactionBlock: tx,
|
||||
options: {
|
||||
showEvents: true,
|
||||
},
|
||||
})
|
||||
.then((result) => result.events!);
|
||||
expect(events).has.length(numMessages);
|
||||
|
||||
for (let i = 0; i < numMessages; ++i) {
|
||||
const eventData = events[i].parsedJson!;
|
||||
expect(eventData.consistency_level).equals(0);
|
||||
expect(eventData.nonce).equals(nonce);
|
||||
expect(eventData.payload).deep.equals([...Buffer.from(payloads[i])]);
|
||||
expect(eventData.sender).equals(emitterCapId);
|
||||
expect(eventData.sequence).equals(i.toString());
|
||||
expect(BigInt(eventData.timestamp) > 0n).is.true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
import { fromB64, normalizeSuiObjectId } from "@mysten/sui.js";
|
||||
import { execSync, ExecSyncOptionsWithStringEncoding } from "child_process";
|
||||
import { UTF8 } from "./consts";
|
||||
|
||||
export const EXEC_UTF8: ExecSyncOptionsWithStringEncoding = { encoding: UTF8 };
|
||||
|
||||
export function buildForBytecode(packagePath: string) {
|
||||
const buildOutput: {
|
||||
modules: string[];
|
||||
dependencies: string[];
|
||||
} = JSON.parse(
|
||||
execSync(
|
||||
`sui move build --dump-bytecode-as-base64 -p ${packagePath} 2> /dev/null`,
|
||||
EXEC_UTF8
|
||||
)
|
||||
);
|
||||
return {
|
||||
modules: buildOutput.modules.map((m: string) => Array.from(fromB64(m))),
|
||||
dependencies: buildOutput.dependencies.map((d: string) =>
|
||||
normalizeSuiObjectId(d)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildForDigest(packagePath: string) {
|
||||
const digest = execSync(
|
||||
`sui move build --dump-package-digest -p ${packagePath} 2> /dev/null`,
|
||||
EXEC_UTF8
|
||||
).substring(0, 64);
|
||||
|
||||
return Buffer.from(digest, "hex");
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// NOTE: modify these to reflect current versions of packages
|
||||
export const VERSION_WORMHOLE = 1;
|
||||
export const VERSION_TOKEN_BRIDGE = 1;
|
||||
|
||||
// keystore
|
||||
export const KEYSTORE = [
|
||||
"AB522qKKEsXMTFRD2SG3Het/02S/ZBOugmcH3R1CDG6l",
|
||||
"AOmPq9B16F3W3ijO/4s9hI6v8LdiYCawKAW31PKpg4Qp",
|
||||
"AGA20wtGcwbcNAG4nwapbQ5wIuXwkYQEWFUoSVAxctHb",
|
||||
];
|
||||
|
||||
// wallets
|
||||
export const WALLET_PRIVATE_KEY = Buffer.from(KEYSTORE[0], "base64").subarray(
|
||||
1
|
||||
);
|
||||
export const RELAYER_PRIVATE_KEY = Buffer.from(KEYSTORE[1], "base64").subarray(
|
||||
1
|
||||
);
|
||||
export const CREATOR_PRIVATE_KEY = Buffer.from(KEYSTORE[2], "base64").subarray(
|
||||
1
|
||||
);
|
||||
|
||||
// guardian signer
|
||||
export const GUARDIAN_PRIVATE_KEY =
|
||||
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
|
||||
|
||||
// wormhole
|
||||
export const WORMHOLE_STATE_ID =
|
||||
"0xc561a02a143575e53b87ba6c1476f053a307eac5179cb1c8121a3d3b220b81c1";
|
||||
|
||||
// token bridge
|
||||
export const TOKEN_BRIDGE_STATE_ID =
|
||||
"0x1c8de839f6331f2d745eb53b1b595bc466b4001c11617b0b66214b2e25ee72fc";
|
||||
|
||||
// governance
|
||||
export const GOVERNANCE_EMITTER =
|
||||
"0000000000000000000000000000000000000000000000000000000000000004";
|
||||
|
||||
// file encoding
|
||||
export const UTF8: BufferEncoding = "utf-8";
|
|
@ -0,0 +1,42 @@
|
|||
export function parseMoveAbort(errorMessage: string) {
|
||||
const parsed = errorMessage.matchAll(
|
||||
/MoveAbort\(MoveLocation { module: ModuleId { address: ([0-9a-f]{64}), name: Identifier\("([A-Za-z_]+)"\) }, function: ([0-9]+), instruction: ([0-9]+), function_name: Some\("([A-Za-z_]+)"\) }, ([0-9]+)\) in command ([0-9]+)/g
|
||||
);
|
||||
|
||||
return parsed.next().value.slice(1, 8);
|
||||
}
|
||||
|
||||
export class MoveAbort {
|
||||
packageId: string;
|
||||
moduleName: string;
|
||||
functionName: string;
|
||||
errorCode: bigint;
|
||||
command: number;
|
||||
|
||||
constructor(
|
||||
packageId: string,
|
||||
moduleName: string,
|
||||
functionName: string,
|
||||
errorCode: string,
|
||||
command: string
|
||||
) {
|
||||
this.packageId = packageId;
|
||||
this.moduleName = moduleName;
|
||||
this.functionName = functionName;
|
||||
this.errorCode = BigInt(errorCode);
|
||||
this.command = Number(command);
|
||||
}
|
||||
|
||||
static parseError(errorMessage: string): MoveAbort {
|
||||
const [packageId, moduleName, , , functionName, errorCode, command] =
|
||||
parseMoveAbort(errorMessage);
|
||||
|
||||
return new MoveAbort(
|
||||
"0x" + packageId,
|
||||
moduleName,
|
||||
functionName,
|
||||
errorCode,
|
||||
command
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { MoveAbort } from "./moveAbort";
|
||||
|
||||
export function parseWormholeError(errorMessage: string) {
|
||||
const abort = MoveAbort.parseError(errorMessage);
|
||||
const code = abort.errorCode;
|
||||
|
||||
switch (abort.moduleName) {
|
||||
case "required_version": {
|
||||
switch (code) {
|
||||
case 0n: {
|
||||
return "E_OUTDATED_VERSION";
|
||||
}
|
||||
default: {
|
||||
throw new Error(`unrecognized error code: ${abort}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
default: {
|
||||
throw new Error(`unrecognized module: ${abort}`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
import * as fs from "fs";
|
||||
import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock";
|
||||
import { GUARDIAN_PRIVATE_KEY, UTF8 } from "./consts";
|
||||
|
||||
export function generateVaaFromDigest(
|
||||
digest: Buffer,
|
||||
governance: mock.GovernanceEmitter
|
||||
) {
|
||||
const timestamp = 12345678;
|
||||
const published = governance.publishWormholeUpgradeContract(
|
||||
timestamp,
|
||||
2,
|
||||
"0x" + digest.toString("hex")
|
||||
);
|
||||
|
||||
// Sui is not supported yet by the SDK, so we need to adjust the payload.
|
||||
published.writeUInt16BE(21, published.length - 34);
|
||||
|
||||
// We will use the signed VAA when we execute the upgrade.
|
||||
const guardians = new mock.MockGuardians(0, [GUARDIAN_PRIVATE_KEY]);
|
||||
return guardians.addSignatures(published, [0]);
|
||||
}
|
||||
|
||||
export function modifyHardCodedVersionControl(
|
||||
packagePath: string,
|
||||
currentVersion: number,
|
||||
newVersion: number
|
||||
) {
|
||||
const versionControlDotMove = `${packagePath}/sources/version_control.move`;
|
||||
|
||||
const contents = fs.readFileSync(versionControlDotMove, UTF8);
|
||||
const src = `const CURRENT_BUILD_VERSION: u64 = ${currentVersion}`;
|
||||
if (contents.indexOf(src) < 0) {
|
||||
throw new Error("current version not found");
|
||||
}
|
||||
|
||||
const dst = `const CURRENT_BUILD_VERSION: u64 = ${newVersion}`;
|
||||
fs.writeFileSync(versionControlDotMove, contents.replace(src, dst), UTF8);
|
||||
}
|
||||
|
||||
export function setUpWormholeDirectory(
|
||||
srcWormholePath: string,
|
||||
dstWormholePath: string
|
||||
) {
|
||||
fs.cpSync(srcWormholePath, dstWormholePath, { recursive: true });
|
||||
|
||||
// Remove irrelevant files. This part is not necessary, but is helpful
|
||||
// for debugging a clean package directory.
|
||||
const removeThese = [
|
||||
"Move.devnet.toml",
|
||||
"Move.lock",
|
||||
"Makefile",
|
||||
"README.md",
|
||||
"build",
|
||||
];
|
||||
for (const basename of removeThese) {
|
||||
fs.rmSync(`${dstWormholePath}/${basename}`, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Fix Move.toml file.
|
||||
const moveTomlPath = `${dstWormholePath}/Move.toml`;
|
||||
const moveToml = fs.readFileSync(moveTomlPath, UTF8);
|
||||
fs.writeFileSync(
|
||||
moveTomlPath,
|
||||
moveToml.replace(`wormhole = "_"`, `wormhole = "0x0"`),
|
||||
UTF8
|
||||
);
|
||||
}
|
||||
|
||||
export function cleanUpPackageDirectory(packagePath: string) {
|
||||
fs.rmSync(packagePath, { recursive: true, force: true });
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
import {
|
||||
RawSigner,
|
||||
SUI_CLOCK_OBJECT_ID,
|
||||
TransactionBlock,
|
||||
} from "@mysten/sui.js";
|
||||
import { buildForBytecode } from "./build";
|
||||
import { getPackageId } from "./utils";
|
||||
|
||||
export async function buildAndUpgradeWormhole(
|
||||
signer: RawSigner,
|
||||
signedVaa: Buffer,
|
||||
wormholePath: string,
|
||||
wormholeStateId: string
|
||||
) {
|
||||
const wormholePackage = await getPackageId(signer.provider, wormholeStateId);
|
||||
|
||||
const tx = new TransactionBlock();
|
||||
|
||||
// Authorize upgrade.
|
||||
const [upgradeTicket] = tx.moveCall({
|
||||
target: `${wormholePackage}::upgrade_contract::authorize_upgrade`,
|
||||
arguments: [
|
||||
tx.object(wormholeStateId),
|
||||
tx.pure(Array.from(signedVaa)),
|
||||
tx.object(SUI_CLOCK_OBJECT_ID),
|
||||
],
|
||||
});
|
||||
|
||||
// Build and generate modules and dependencies for upgrade.
|
||||
const { modules, dependencies } = buildForBytecode(wormholePath);
|
||||
const [upgradeReceipt] = tx.upgrade({
|
||||
modules,
|
||||
dependencies,
|
||||
packageId: wormholePackage,
|
||||
ticket: upgradeTicket,
|
||||
});
|
||||
|
||||
// Commit upgrade.
|
||||
tx.moveCall({
|
||||
target: `${wormholePackage}::upgrade_contract::commit_upgrade`,
|
||||
arguments: [tx.object(wormholeStateId), upgradeReceipt],
|
||||
});
|
||||
|
||||
// Cannot auto compute gas budget, so we need to configure it manually.
|
||||
// Gas ~215m.
|
||||
tx.setGasBudget(215_000_000n);
|
||||
|
||||
return signer.signAndExecuteTransactionBlock({
|
||||
transactionBlock: tx,
|
||||
options: {
|
||||
showEffects: true,
|
||||
showEvents: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function migrate(signer: RawSigner, stateId: string) {
|
||||
const contractPackage = await getPackageId(signer.provider, stateId);
|
||||
|
||||
const tx = new TransactionBlock();
|
||||
tx.moveCall({
|
||||
target: `${contractPackage}::migrate::migrate`,
|
||||
arguments: [tx.object(stateId)],
|
||||
});
|
||||
|
||||
return signer.signAndExecuteTransactionBlock({
|
||||
transactionBlock: tx,
|
||||
options: {
|
||||
showEffects: true,
|
||||
showEvents: true,
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { JsonRpcProvider } from "@mysten/sui.js";
|
||||
|
||||
export async function getPackageId(
|
||||
provider: JsonRpcProvider,
|
||||
stateId: string
|
||||
): Promise<string> {
|
||||
const state = await provider
|
||||
.getObject({
|
||||
id: stateId,
|
||||
options: {
|
||||
showContent: true,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data?.content?.dataType == "moveObject") {
|
||||
return result.data.content.fields;
|
||||
}
|
||||
|
||||
throw new Error("not move object");
|
||||
});
|
||||
|
||||
if ("upgrade_cap" in state) {
|
||||
return state.upgrade_cap.fields.package;
|
||||
}
|
||||
|
||||
throw new Error("upgrade_cap not found");
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import { SUI_CLOCK_OBJECT_ID, TransactionBlock } from "@mysten/sui.js";
|
||||
|
||||
export function addPrepareMessageAndPublishMessage(
|
||||
tx: TransactionBlock,
|
||||
wormholePackage: string,
|
||||
wormholeStateId: string,
|
||||
emitterCapId: string,
|
||||
nonce: number,
|
||||
payload: number[] | string
|
||||
): TransactionBlock {
|
||||
const [feeAmount] = tx.moveCall({
|
||||
target: `${wormholePackage}::state::message_fee`,
|
||||
arguments: [tx.object(wormholeStateId)],
|
||||
});
|
||||
const [wormholeFee] = tx.splitCoins(tx.gas, [feeAmount]);
|
||||
const [messageTicket] = tx.moveCall({
|
||||
target: `${wormholePackage}::publish_message::prepare_message`,
|
||||
arguments: [tx.object(emitterCapId), tx.pure(nonce), tx.pure(payload)],
|
||||
});
|
||||
tx.moveCall({
|
||||
target: `${wormholePackage}::publish_message::publish_message`,
|
||||
arguments: [
|
||||
tx.object(wormholeStateId),
|
||||
wormholeFee,
|
||||
messageTicket,
|
||||
tx.object(SUI_CLOCK_OBJECT_ID),
|
||||
],
|
||||
});
|
||||
|
||||
return tx;
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "@wormhole-foundation/wormhole-sui-integration-test",
|
||||
"version": "0.0.1",
|
||||
"description": "Wormhole Sui Integration Test",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@certusone/wormhole-sdk": "^0.9.12",
|
||||
"@mysten/sui.js": "^0.32.2",
|
||||
"chai": "^4.3.7",
|
||||
"mocha": "^10.2.0",
|
||||
"prettier": "^2.8.7",
|
||||
"ts-mocha": "^10.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"@types/node": "^18.15.11"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
#/bin/bash
|
||||
|
||||
pgrep -f sui > /dev/null
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "sui local validator already running"
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
TEST_DIR=$(dirname $0)
|
||||
SUI_CONFIG=$TEST_DIR/sui_config
|
||||
|
||||
### Remove databases generated by localnet
|
||||
rm -rf $SUI_CONFIG/*_db
|
||||
|
||||
### Start local node
|
||||
echo "$(date) :: starting localnet"
|
||||
sui start --network.config $SUI_CONFIG/network.yaml > /dev/null 2>&1 &
|
||||
sleep 1
|
||||
|
||||
echo "$(date) :: deploying wormhole and token bridge"
|
||||
cd $TEST_DIR/..
|
||||
bash scripts/deploy.sh devnet \
|
||||
-k AGA20wtGcwbcNAG4nwapbQ5wIuXwkYQEWFUoSVAxctHb > deploy.out 2>&1
|
||||
cd testing
|
||||
|
||||
## run contract tests here
|
||||
echo "$(date) :: running tests"
|
||||
npx ts-mocha -t 1000000 $TEST_DIR/js/*.ts
|
||||
|
||||
# nuke
|
||||
echo "$(date) :: done"
|
||||
pkill sui
|
||||
|
||||
# remove databases generated by localnet
|
||||
rm -rf $SUI_CONFIG/*_db
|
|
@ -0,0 +1,300 @@
|
|||
import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock";
|
||||
import {
|
||||
RawSigner,
|
||||
SUI_CLOCK_OBJECT_ID,
|
||||
TransactionBlock,
|
||||
fromB64,
|
||||
normalizeSuiObjectId,
|
||||
JsonRpcProvider,
|
||||
Ed25519Keypair,
|
||||
testnetConnection,
|
||||
} from "@mysten/sui.js";
|
||||
import { execSync } from "child_process";
|
||||
import { resolve } from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const GOVERNANCE_EMITTER =
|
||||
"0000000000000000000000000000000000000000000000000000000000000004";
|
||||
|
||||
const TOKEN_BRIDGE_STATE_ID =
|
||||
"0x32422cb2f929b6a4e3f81b4791ea11ac2af896b310f3d9442aa1fe924ce0bab4";
|
||||
const WORMHOLE_STATE_ID =
|
||||
"0x69ae41bdef4770895eb4e7aaefee5e4673acc08f6917b4856cf55549c4573ca8";
|
||||
|
||||
async function main() {
|
||||
const guardianPrivateKey = process.env.TESTNET_GUARDIAN_PRIVATE_KEY;
|
||||
if (guardianPrivateKey === undefined) {
|
||||
throw new Error("TESTNET_GUARDIAN_PRIVATE_KEY unset in environment");
|
||||
}
|
||||
|
||||
const walletPrivateKey = process.env.TESTNET_WALLET_PRIVATE_KEY;
|
||||
if (walletPrivateKey === undefined) {
|
||||
throw new Error("TESTNET_WALLET_PRIVATE_KEY unset in environment");
|
||||
}
|
||||
|
||||
const provider = new JsonRpcProvider(testnetConnection);
|
||||
const wallet = new RawSigner(
|
||||
Ed25519Keypair.fromSecretKey(
|
||||
Buffer.from(walletPrivateKey, "base64").subarray(1)
|
||||
),
|
||||
provider
|
||||
);
|
||||
|
||||
const dstTokenBridgePath = resolve(`${__dirname}/../../token_bridge`);
|
||||
|
||||
// Build for digest.
|
||||
const { modules, dependencies, digest } =
|
||||
buildForBytecodeAndDigest(dstTokenBridgePath);
|
||||
console.log("dependencies", dependencies);
|
||||
console.log("digest", digest.toString("hex"));
|
||||
|
||||
// We will use the signed VAA when we execute the upgrade.
|
||||
const guardians = new mock.MockGuardians(0, [guardianPrivateKey]);
|
||||
|
||||
const timestamp = 12345678;
|
||||
const governance = new mock.GovernanceEmitter(GOVERNANCE_EMITTER);
|
||||
const published = governance.publishWormholeUpgradeContract(
|
||||
timestamp,
|
||||
2,
|
||||
"0x" + digest.toString("hex")
|
||||
);
|
||||
const moduleName = Buffer.alloc(32);
|
||||
moduleName.write("TokenBridge", 32 - "TokenBridge".length);
|
||||
published.write(moduleName.toString(), 84 - 33);
|
||||
published.writeUInt16BE(21, 84);
|
||||
published.writeUInt8(2, 83);
|
||||
//message.writeUInt8(1, 83);
|
||||
published.writeUInt16BE(21, published.length - 34);
|
||||
|
||||
const signedVaa = guardians.addSignatures(published, [0]);
|
||||
console.log("Upgrade VAA:", signedVaa.toString("hex"));
|
||||
|
||||
// // And execute upgrade with governance VAA.
|
||||
// const upgradeResults = await upgradeTokenBridge(
|
||||
// wallet,
|
||||
// TOKEN_BRIDGE_STATE_ID,
|
||||
// WORMHOLE_STATE_ID,
|
||||
// modules,
|
||||
// dependencies,
|
||||
// signedVaa
|
||||
// );
|
||||
|
||||
// console.log("tx digest", upgradeResults.digest);
|
||||
// console.log("tx effects", JSON.stringify(upgradeResults.effects!));
|
||||
// console.log("tx events", JSON.stringify(upgradeResults.events!));
|
||||
|
||||
// TODO: grab new package ID from the events above. Do not rely on the RPC
|
||||
// call because it may give you a stale package ID after the upgrade.
|
||||
|
||||
const migrateResults = await migrateTokenBridge(
|
||||
wallet,
|
||||
TOKEN_BRIDGE_STATE_ID,
|
||||
WORMHOLE_STATE_ID,
|
||||
signedVaa
|
||||
);
|
||||
console.log("tx digest", migrateResults.digest);
|
||||
console.log("tx effects", JSON.stringify(migrateResults.effects!));
|
||||
console.log("tx events", JSON.stringify(migrateResults.events!));
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
// Yeah buddy.
|
||||
|
||||
function buildForBytecodeAndDigest(packagePath: string) {
|
||||
const buildOutput: {
|
||||
modules: string[];
|
||||
dependencies: string[];
|
||||
digest: number[];
|
||||
} = JSON.parse(
|
||||
execSync(
|
||||
`sui move build --dump-bytecode-as-base64 -p ${packagePath} 2> /dev/null`,
|
||||
{ encoding: "utf-8" }
|
||||
)
|
||||
);
|
||||
return {
|
||||
modules: buildOutput.modules.map((m: string) => Array.from(fromB64(m))),
|
||||
dependencies: buildOutput.dependencies.map((d: string) =>
|
||||
normalizeSuiObjectId(d)
|
||||
),
|
||||
digest: Buffer.from(buildOutput.digest),
|
||||
};
|
||||
}
|
||||
|
||||
async function getPackageId(
|
||||
provider: JsonRpcProvider,
|
||||
stateId: string
|
||||
): Promise<string> {
|
||||
const state = await provider
|
||||
.getObject({
|
||||
id: stateId,
|
||||
options: {
|
||||
showContent: true,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data?.content?.dataType == "moveObject") {
|
||||
return result.data.content.fields;
|
||||
}
|
||||
|
||||
throw new Error("not move object");
|
||||
});
|
||||
|
||||
if ("upgrade_cap" in state) {
|
||||
return state.upgrade_cap.fields.package;
|
||||
}
|
||||
|
||||
throw new Error("upgrade_cap not found");
|
||||
}
|
||||
|
||||
async function upgradeTokenBridge(
|
||||
signer: RawSigner,
|
||||
tokenBridgeStateId: string,
|
||||
wormholeStateId: string,
|
||||
modules: number[][],
|
||||
dependencies: string[],
|
||||
signedVaa: Buffer
|
||||
) {
|
||||
const tokenBridgePackage = await getPackageId(
|
||||
signer.provider,
|
||||
tokenBridgeStateId
|
||||
);
|
||||
const wormholePackage = await getPackageId(signer.provider, wormholeStateId);
|
||||
|
||||
const tx = new TransactionBlock();
|
||||
|
||||
const [verifiedVaa] = tx.moveCall({
|
||||
target: `${wormholePackage}::vaa::parse_and_verify`,
|
||||
arguments: [
|
||||
tx.object(wormholeStateId),
|
||||
tx.pure(Array.from(signedVaa)),
|
||||
tx.object(SUI_CLOCK_OBJECT_ID),
|
||||
],
|
||||
});
|
||||
const [decreeTicket] = tx.moveCall({
|
||||
target: `${tokenBridgePackage}::upgrade_contract::authorize_governance`,
|
||||
arguments: [tx.object(tokenBridgeStateId)],
|
||||
});
|
||||
const [decreeReceipt] = tx.moveCall({
|
||||
target: `${wormholePackage}::governance_message::verify_vaa`,
|
||||
arguments: [tx.object(wormholeStateId), verifiedVaa, decreeTicket],
|
||||
typeArguments: [
|
||||
`${tokenBridgePackage}::upgrade_contract::GovernanceWitness`,
|
||||
],
|
||||
});
|
||||
|
||||
// Authorize upgrade.
|
||||
const [upgradeTicket] = tx.moveCall({
|
||||
target: `${tokenBridgePackage}::upgrade_contract::authorize_upgrade`,
|
||||
arguments: [tx.object(tokenBridgeStateId), decreeReceipt],
|
||||
});
|
||||
|
||||
// Build and generate modules and dependencies for upgrade.
|
||||
const [upgradeReceipt] = tx.upgrade({
|
||||
modules,
|
||||
dependencies,
|
||||
packageId: tokenBridgePackage,
|
||||
ticket: upgradeTicket,
|
||||
});
|
||||
|
||||
// Commit upgrade.
|
||||
tx.moveCall({
|
||||
target: `${tokenBridgePackage}::upgrade_contract::commit_upgrade`,
|
||||
arguments: [tx.object(tokenBridgeStateId), upgradeReceipt],
|
||||
});
|
||||
|
||||
// Cannot auto compute gas budget, so we need to configure it manually.
|
||||
// Gas ~215m.
|
||||
//tx.setGasBudget(1_000_000_000n);
|
||||
|
||||
return signer.signAndExecuteTransactionBlock({
|
||||
transactionBlock: tx,
|
||||
options: {
|
||||
showEffects: true,
|
||||
showEvents: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function migrateTokenBridge(
|
||||
signer: RawSigner,
|
||||
tokenBridgeStateId: string,
|
||||
wormholeStateId: string,
|
||||
signedUpgradeVaa: Buffer
|
||||
) {
|
||||
const tokenBridgePackage = await getPackageId(
|
||||
signer.provider,
|
||||
tokenBridgeStateId
|
||||
);
|
||||
const wormholePackage = await getPackageId(signer.provider, wormholeStateId);
|
||||
|
||||
const tx = new TransactionBlock();
|
||||
|
||||
const [verifiedVaa] = tx.moveCall({
|
||||
target: `${wormholePackage}::vaa::parse_and_verify`,
|
||||
arguments: [
|
||||
tx.object(wormholeStateId),
|
||||
tx.pure(Array.from(signedUpgradeVaa)),
|
||||
tx.object(SUI_CLOCK_OBJECT_ID),
|
||||
],
|
||||
});
|
||||
const [decreeTicket] = tx.moveCall({
|
||||
target: `${tokenBridgePackage}::upgrade_contract::authorize_governance`,
|
||||
arguments: [tx.object(tokenBridgeStateId)],
|
||||
});
|
||||
const [decreeReceipt] = tx.moveCall({
|
||||
target: `${wormholePackage}::governance_message::verify_vaa`,
|
||||
arguments: [tx.object(wormholeStateId), verifiedVaa, decreeTicket],
|
||||
typeArguments: [
|
||||
`${tokenBridgePackage}::upgrade_contract::GovernanceWitness`,
|
||||
],
|
||||
});
|
||||
tx.moveCall({
|
||||
target: `${tokenBridgePackage}::migrate::migrate`,
|
||||
arguments: [tx.object(tokenBridgeStateId), decreeReceipt],
|
||||
});
|
||||
|
||||
return signer.signAndExecuteTransactionBlock({
|
||||
transactionBlock: tx,
|
||||
options: {
|
||||
showEffects: true,
|
||||
showEvents: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setUpWormholeDirectory(
|
||||
srcWormholePath: string,
|
||||
dstWormholePath: string
|
||||
) {
|
||||
fs.cpSync(srcWormholePath, dstWormholePath, { recursive: true });
|
||||
|
||||
// Remove irrelevant files. This part is not necessary, but is helpful
|
||||
// for debugging a clean package directory.
|
||||
const removeThese = [
|
||||
"Move.devnet.toml",
|
||||
"Move.lock",
|
||||
"Makefile",
|
||||
"README.md",
|
||||
"build",
|
||||
];
|
||||
for (const basename of removeThese) {
|
||||
fs.rmSync(`${dstWormholePath}/${basename}`, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Fix Move.toml file.
|
||||
const moveTomlPath = `${dstWormholePath}/Move.toml`;
|
||||
const moveToml = fs.readFileSync(moveTomlPath, "utf-8");
|
||||
fs.writeFileSync(
|
||||
moveTomlPath,
|
||||
moveToml.replace(`wormhole = "_"`, `wormhole = "0x0"`),
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
function cleanUpPackageDirectory(packagePath: string) {
|
||||
fs.rmSync(packagePath, { recursive: true, force: true });
|
||||
}
|
|
@ -0,0 +1,267 @@
|
|||
import * as mock from "@certusone/wormhole-sdk/lib/cjs/mock";
|
||||
import {
|
||||
RawSigner,
|
||||
SUI_CLOCK_OBJECT_ID,
|
||||
TransactionBlock,
|
||||
fromB64,
|
||||
normalizeSuiObjectId,
|
||||
JsonRpcProvider,
|
||||
Ed25519Keypair,
|
||||
testnetConnection,
|
||||
} from "@mysten/sui.js";
|
||||
import { execSync } from "child_process";
|
||||
import { resolve } from "path";
|
||||
import * as fs from "fs";
|
||||
|
||||
const GOVERNANCE_EMITTER =
|
||||
"0000000000000000000000000000000000000000000000000000000000000004";
|
||||
|
||||
const WORMHOLE_STATE_ID =
|
||||
"0x69ae41bdef4770895eb4e7aaefee5e4673acc08f6917b4856cf55549c4573ca8";
|
||||
|
||||
async function main() {
|
||||
const guardianPrivateKey = process.env.TESTNET_GUARDIAN_PRIVATE_KEY;
|
||||
if (guardianPrivateKey === undefined) {
|
||||
throw new Error("TESTNET_GUARDIAN_PRIVATE_KEY unset in environment");
|
||||
}
|
||||
|
||||
const walletPrivateKey = process.env.TESTNET_WALLET_PRIVATE_KEY;
|
||||
if (walletPrivateKey === undefined) {
|
||||
throw new Error("TESTNET_WALLET_PRIVATE_KEY unset in environment");
|
||||
}
|
||||
|
||||
const provider = new JsonRpcProvider(testnetConnection);
|
||||
const wallet = new RawSigner(
|
||||
Ed25519Keypair.fromSecretKey(
|
||||
Buffer.from(walletPrivateKey, "base64").subarray(1)
|
||||
),
|
||||
provider
|
||||
);
|
||||
|
||||
const srcWormholePath = resolve(`${__dirname}/../../wormhole`);
|
||||
const dstWormholePath = resolve(`${__dirname}/wormhole`);
|
||||
|
||||
// Stage build(s).
|
||||
setUpWormholeDirectory(srcWormholePath, dstWormholePath);
|
||||
|
||||
// Build for digest.
|
||||
const { modules, dependencies, digest } =
|
||||
buildForBytecodeAndDigest(dstWormholePath);
|
||||
|
||||
// We will use the signed VAA when we execute the upgrade.
|
||||
const guardians = new mock.MockGuardians(0, [guardianPrivateKey]);
|
||||
|
||||
const timestamp = 12345678;
|
||||
const governance = new mock.GovernanceEmitter(GOVERNANCE_EMITTER);
|
||||
const published = governance.publishWormholeUpgradeContract(
|
||||
timestamp,
|
||||
2,
|
||||
"0x" + digest.toString("hex")
|
||||
);
|
||||
published.writeUInt16BE(21, published.length - 34);
|
||||
|
||||
const signedVaa = guardians.addSignatures(published, [0]);
|
||||
console.log("Upgrade VAA:", signedVaa.toString("hex"));
|
||||
|
||||
// And execute upgrade with governance VAA.
|
||||
const upgradeResults = await buildAndUpgradeWormhole(
|
||||
wallet,
|
||||
WORMHOLE_STATE_ID,
|
||||
modules,
|
||||
dependencies,
|
||||
signedVaa
|
||||
);
|
||||
|
||||
console.log("tx digest", upgradeResults.digest);
|
||||
console.log("tx effects", JSON.stringify(upgradeResults.effects!));
|
||||
console.log("tx events", JSON.stringify(upgradeResults.events!));
|
||||
|
||||
// TODO: grab new package ID from the events above. Do not rely on the RPC
|
||||
// call because it may give you a stale package ID after the upgrade.
|
||||
|
||||
// const migrateResults = await migrateWormhole(
|
||||
// wallet,
|
||||
// WORMHOLE_STATE_ID,
|
||||
// signedVaa
|
||||
// );
|
||||
// console.log("tx digest", migrateResults.digest);
|
||||
// console.log("tx effects", JSON.stringify(migrateResults.effects!));
|
||||
// console.log("tx events", JSON.stringify(migrateResults.events!));
|
||||
|
||||
// Clean up.
|
||||
cleanUpPackageDirectory(dstWormholePath);
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
// Yeah buddy.
|
||||
|
||||
function buildForBytecodeAndDigest(packagePath: string) {
|
||||
const buildOutput: {
|
||||
modules: string[];
|
||||
dependencies: string[];
|
||||
digest: number[];
|
||||
} = JSON.parse(
|
||||
execSync(
|
||||
`sui move build --dump-bytecode-as-base64 -p ${packagePath} 2> /dev/null`,
|
||||
{ encoding: "utf-8" }
|
||||
)
|
||||
);
|
||||
return {
|
||||
modules: buildOutput.modules.map((m: string) => Array.from(fromB64(m))),
|
||||
dependencies: buildOutput.dependencies.map((d: string) =>
|
||||
normalizeSuiObjectId(d)
|
||||
),
|
||||
digest: Buffer.from(buildOutput.digest),
|
||||
};
|
||||
}
|
||||
|
||||
async function getPackageId(
|
||||
provider: JsonRpcProvider,
|
||||
stateId: string
|
||||
): Promise<string> {
|
||||
const state = await provider
|
||||
.getObject({
|
||||
id: stateId,
|
||||
options: {
|
||||
showContent: true,
|
||||
},
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.data?.content?.dataType == "moveObject") {
|
||||
return result.data.content.fields;
|
||||
}
|
||||
|
||||
throw new Error("not move object");
|
||||
});
|
||||
|
||||
if ("upgrade_cap" in state) {
|
||||
return state.upgrade_cap.fields.package;
|
||||
}
|
||||
|
||||
throw new Error("upgrade_cap not found");
|
||||
}
|
||||
|
||||
async function buildAndUpgradeWormhole(
|
||||
signer: RawSigner,
|
||||
wormholeStateId: string,
|
||||
modules: number[][],
|
||||
dependencies: string[],
|
||||
signedVaa: Buffer
|
||||
) {
|
||||
const wormholePackage = await getPackageId(signer.provider, wormholeStateId);
|
||||
|
||||
const tx = new TransactionBlock();
|
||||
|
||||
const [verifiedVaa] = tx.moveCall({
|
||||
target: `${wormholePackage}::vaa::parse_and_verify`,
|
||||
arguments: [
|
||||
tx.object(wormholeStateId),
|
||||
tx.pure(Array.from(signedVaa)),
|
||||
tx.object(SUI_CLOCK_OBJECT_ID),
|
||||
],
|
||||
});
|
||||
const [decreeTicket] = tx.moveCall({
|
||||
target: `${wormholePackage}::upgrade_contract::authorize_governance`,
|
||||
arguments: [tx.object(wormholeStateId)],
|
||||
});
|
||||
const [decreeReceipt] = tx.moveCall({
|
||||
target: `${wormholePackage}::governance_message::verify_vaa`,
|
||||
arguments: [tx.object(wormholeStateId), verifiedVaa, decreeTicket],
|
||||
typeArguments: [`${wormholePackage}::upgrade_contract::GovernanceWitness`],
|
||||
});
|
||||
|
||||
// Authorize upgrade.
|
||||
const [upgradeTicket] = tx.moveCall({
|
||||
target: `${wormholePackage}::upgrade_contract::authorize_upgrade`,
|
||||
arguments: [tx.object(wormholeStateId), decreeReceipt],
|
||||
});
|
||||
|
||||
// Build and generate modules and dependencies for upgrade.
|
||||
const [upgradeReceipt] = tx.upgrade({
|
||||
modules,
|
||||
dependencies,
|
||||
packageId: wormholePackage,
|
||||
ticket: upgradeTicket,
|
||||
});
|
||||
|
||||
// Commit upgrade.
|
||||
tx.moveCall({
|
||||
target: `${wormholePackage}::upgrade_contract::commit_upgrade`,
|
||||
arguments: [tx.object(wormholeStateId), upgradeReceipt],
|
||||
});
|
||||
|
||||
// Cannot auto compute gas budget, so we need to configure it manually.
|
||||
// Gas ~215m.
|
||||
//tx.setGasBudget(1_000_000_000n);
|
||||
|
||||
return signer.signAndExecuteTransactionBlock({
|
||||
transactionBlock: tx,
|
||||
options: {
|
||||
showEffects: true,
|
||||
showEvents: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function migrateWormhole(
|
||||
signer: RawSigner,
|
||||
wormholeStateId: string,
|
||||
signedUpgradeVaa: Buffer
|
||||
) {
|
||||
const contractPackage = await getPackageId(signer.provider, wormholeStateId);
|
||||
|
||||
const tx = new TransactionBlock();
|
||||
tx.moveCall({
|
||||
target: `${contractPackage}::migrate::migrate`,
|
||||
arguments: [
|
||||
tx.object(wormholeStateId),
|
||||
tx.pure(Array.from(signedUpgradeVaa)),
|
||||
tx.object(SUI_CLOCK_OBJECT_ID),
|
||||
],
|
||||
});
|
||||
|
||||
return signer.signAndExecuteTransactionBlock({
|
||||
transactionBlock: tx,
|
||||
options: {
|
||||
showEffects: true,
|
||||
showEvents: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setUpWormholeDirectory(
|
||||
srcWormholePath: string,
|
||||
dstWormholePath: string
|
||||
) {
|
||||
fs.cpSync(srcWormholePath, dstWormholePath, { recursive: true });
|
||||
|
||||
// Remove irrelevant files. This part is not necessary, but is helpful
|
||||
// for debugging a clean package directory.
|
||||
const removeThese = [
|
||||
"Move.devnet.toml",
|
||||
"Move.lock",
|
||||
"Makefile",
|
||||
"README.md",
|
||||
"build",
|
||||
];
|
||||
for (const basename of removeThese) {
|
||||
fs.rmSync(`${dstWormholePath}/${basename}`, {
|
||||
recursive: true,
|
||||
force: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Fix Move.toml file.
|
||||
const moveTomlPath = `${dstWormholePath}/Move.toml`;
|
||||
const moveToml = fs.readFileSync(moveTomlPath, "utf-8");
|
||||
fs.writeFileSync(
|
||||
moveTomlPath,
|
||||
moveToml.replace(`wormhole = "_"`, `wormhole = "0x0"`),
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
function cleanUpPackageDirectory(packagePath: string) {
|
||||
fs.rmSync(packagePath, { recursive: true, force: true });
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
keystore:
|
||||
File: sui_config/sui.keystore
|
||||
envs:
|
||||
- alias: localnet
|
||||
rpc: "http://0.0.0.0:9000"
|
||||
ws: ~
|
||||
- alias: devnet
|
||||
rpc: "https://fullnode.devnet.sui.io:443"
|
||||
ws: ~
|
||||
active_env: localnet
|
||||
active_address: "0xed867315e3f7c83ae82e6d5858b6a6cc57c291fd84f7509646ebc8162169cf96"
|
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: W+hPTVWhdFgzHs3YuRHV6gLfgFhHA1WG0pisIXiN8E8=
|
||||
worker-key-pair:
|
||||
value: AApEvpZE1O+2GMqZ1AbRE3+Kmgr1O5mdsMZ6I/gLpVSy
|
||||
account-key-pair:
|
||||
value: AN7ZHgjN8G7Nw7Q8NtY9TisPBjmEYpdUzbczjqR98XLh
|
||||
network-key-pair:
|
||||
value: AAnB6/zZooq4xDtB7oM/GeTSCh5tBxKAyJwWOMPlEJ4R
|
||||
db-path: sui_config/authorities_db/full_node_db
|
||||
network-address: /ip4/127.0.0.1/tcp/36683/http
|
||||
json-rpc-address: "0.0.0.0:9000"
|
||||
metrics-address: "127.0.0.1:35915"
|
||||
admin-interface-port: 44319
|
||||
enable-event-processing: true
|
||||
enable-index-processing: true
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: ~
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:38187"
|
||||
external-address: /ip4/127.0.0.1/udp/38187
|
||||
seed-peers:
|
||||
- peer-id: ce60e3077e02a3683436af450f3a4511b4c40b158956637caf9ccf11391e7e10
|
||||
address: /ip4/127.0.0.1/udp/44061
|
||||
- peer-id: 5f0f42cb3fb20dd577703388320964f9351d997313c04a032247060d214b2e71
|
||||
address: /ip4/127.0.0.1/udp/46335
|
||||
- peer-id: 6d9095130b1536c0c9218ea9feb0f36685a6fa0b3b1e67d256cc4fb340a48d69
|
||||
address: /ip4/127.0.0.1/udp/32965
|
||||
- peer-id: b2915bf787845a55c24e18fdc162a575eb02d23bae3f9e566d7c51ebcfeb4a42
|
||||
address: /ip4/127.0.0.1/udp/39889
|
||||
genesis:
|
||||
genesis-file-location: sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 2
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 30
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
||||
expensive-safety-check-config:
|
||||
enable-epoch-sui-conservation-check: false
|
||||
enable-deep-per-tx-sui-conservation-check: false
|
||||
force-disable-epoch-sui-conservation-check: false
|
||||
enable-state-consistency-check: false
|
||||
force-disable-state-consistency-check: false
|
||||
enable-move-vm-paranoid-checks: false
|
Binary file not shown.
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,7 @@
|
|||
[
|
||||
"AB522qKKEsXMTFRD2SG3Het/02S/ZBOugmcH3R1CDG6l",
|
||||
"AOmPq9B16F3W3ijO/4s9hI6v8LdiYCawKAW31PKpg4Qp",
|
||||
"AOLhc0ryVWnD5LmqH3kCHruBpVV+68EWjEGu2eC9gndK",
|
||||
"AKCo1FyhQ0zUpnoZLmGJJ+8LttTrt56W87Ho4vBF+R+8",
|
||||
"AGA20wtGcwbcNAG4nwapbQ5wIuXwkYQEWFUoSVAxctHb"
|
||||
]
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: VTDx4HjVmRBqdqBWg2zN+zcFE20io3CrBchGy/iV1lo=
|
||||
worker-key-pair:
|
||||
value: ABlC9PMmIQHjxila3AEOXDxwCSuodcvJh2Q5O5HIB00K
|
||||
account-key-pair:
|
||||
value: AIV4Ng6OYQf6irjVCZly5X7dSpdFpwoWtdAx9u4PANRl
|
||||
network-key-pair:
|
||||
value: AOqJl2rHMnroe26vjkkIuWGBD/y6HzQG6MK5bC9njF0s
|
||||
db-path: sui_config/authorities_db/99f25ef61f80
|
||||
network-address: /ip4/127.0.0.1/tcp/36459/http
|
||||
json-rpc-address: "127.0.0.1:38133"
|
||||
metrics-address: "127.0.0.1:44135"
|
||||
admin-interface-port: 33917
|
||||
consensus-config:
|
||||
address: /ip4/127.0.0.1/tcp/41459/http
|
||||
db-path: sui_config/consensus_db/99f25ef61f80
|
||||
internal-worker-address: ~
|
||||
max-pending-transactions: ~
|
||||
narwhal-config:
|
||||
header_num_of_batches_threshold: 32
|
||||
max_header_num_of_batches: 1000
|
||||
max_header_delay: 2000ms
|
||||
min_header_delay: 500ms
|
||||
gc_depth: 50
|
||||
sync_retry_delay: 5000ms
|
||||
sync_retry_nodes: 3
|
||||
batch_size: 500000
|
||||
max_batch_delay: 100ms
|
||||
block_synchronizer:
|
||||
range_synchronize_timeout: 30000ms
|
||||
certificates_synchronize_timeout: 30000ms
|
||||
payload_synchronize_timeout: 30000ms
|
||||
payload_availability_timeout: 30000ms
|
||||
handler_certificate_deliver_timeout: 30000ms
|
||||
consensus_api_grpc:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/44689/http
|
||||
get_collections_timeout: 5000ms
|
||||
remove_collections_timeout: 5000ms
|
||||
max_concurrent_requests: 500000
|
||||
prometheus_metrics:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/33219/http
|
||||
network_admin_server:
|
||||
primary_network_admin_server_port: 33945
|
||||
worker_network_admin_server_base_port: 38081
|
||||
anemo:
|
||||
send_certificate_rate_limit: ~
|
||||
get_payload_availability_rate_limit: ~
|
||||
get_certificates_rate_limit: ~
|
||||
report_batch_rate_limit: ~
|
||||
request_batch_rate_limit: ~
|
||||
enable-event-processing: false
|
||||
enable-index-processing: true
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: 20000000000
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:44061"
|
||||
external-address: /ip4/127.0.0.1/udp/44061
|
||||
genesis:
|
||||
genesis-file-location: sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 2
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 30
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
||||
expensive-safety-check-config:
|
||||
enable-epoch-sui-conservation-check: false
|
||||
enable-deep-per-tx-sui-conservation-check: false
|
||||
force-disable-epoch-sui-conservation-check: false
|
||||
enable-state-consistency-check: false
|
||||
force-disable-state-consistency-check: false
|
||||
enable-move-vm-paranoid-checks: false
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: avYcyVgYMXTyaUYh9IRwLK0gSzl7YF6ZQDAbrS1Bhvo=
|
||||
worker-key-pair:
|
||||
value: AGsxCVxeIZ6fscvGECzV93Hi4JkqM4zMYEA8wBGfXQrz
|
||||
account-key-pair:
|
||||
value: AF9cOMxTRAUTOws2M8W5slHf41HITA+M3nqXHT6nlH6S
|
||||
network-key-pair:
|
||||
value: ALH/8qz2YlwAuxY/hOvuXiglYq0e4LLU1/lyf5uKgPY8
|
||||
db-path: sui_config/authorities_db/8dcff6d15504
|
||||
network-address: /ip4/127.0.0.1/tcp/33355/http
|
||||
json-rpc-address: "127.0.0.1:39573"
|
||||
metrics-address: "127.0.0.1:45851"
|
||||
admin-interface-port: 35739
|
||||
consensus-config:
|
||||
address: /ip4/127.0.0.1/tcp/42959/http
|
||||
db-path: sui_config/consensus_db/8dcff6d15504
|
||||
internal-worker-address: ~
|
||||
max-pending-transactions: ~
|
||||
narwhal-config:
|
||||
header_num_of_batches_threshold: 32
|
||||
max_header_num_of_batches: 1000
|
||||
max_header_delay: 2000ms
|
||||
min_header_delay: 500ms
|
||||
gc_depth: 50
|
||||
sync_retry_delay: 5000ms
|
||||
sync_retry_nodes: 3
|
||||
batch_size: 500000
|
||||
max_batch_delay: 100ms
|
||||
block_synchronizer:
|
||||
range_synchronize_timeout: 30000ms
|
||||
certificates_synchronize_timeout: 30000ms
|
||||
payload_synchronize_timeout: 30000ms
|
||||
payload_availability_timeout: 30000ms
|
||||
handler_certificate_deliver_timeout: 30000ms
|
||||
consensus_api_grpc:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/37001/http
|
||||
get_collections_timeout: 5000ms
|
||||
remove_collections_timeout: 5000ms
|
||||
max_concurrent_requests: 500000
|
||||
prometheus_metrics:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/39831/http
|
||||
network_admin_server:
|
||||
primary_network_admin_server_port: 39853
|
||||
worker_network_admin_server_base_port: 36429
|
||||
anemo:
|
||||
send_certificate_rate_limit: ~
|
||||
get_payload_availability_rate_limit: ~
|
||||
get_certificates_rate_limit: ~
|
||||
report_batch_rate_limit: ~
|
||||
request_batch_rate_limit: ~
|
||||
enable-event-processing: false
|
||||
enable-index-processing: true
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: 20000000000
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:46335"
|
||||
external-address: /ip4/127.0.0.1/udp/46335
|
||||
genesis:
|
||||
genesis-file-location: sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 2
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 30
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
||||
expensive-safety-check-config:
|
||||
enable-epoch-sui-conservation-check: false
|
||||
enable-deep-per-tx-sui-conservation-check: false
|
||||
force-disable-epoch-sui-conservation-check: false
|
||||
enable-state-consistency-check: false
|
||||
force-disable-state-consistency-check: false
|
||||
enable-move-vm-paranoid-checks: false
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: OXnx3yM1C/ppgnDMx/o1d49fJs7E05kq11mXNae/O+I=
|
||||
worker-key-pair:
|
||||
value: AHXs8DP7EccyxtxAGq/m33LgvOApXs4JStH3PLAe9vGw
|
||||
account-key-pair:
|
||||
value: AC8vF9E3QYf0aTyBZWlSzJJXETvV5vYkOtEJl+DWQMlk
|
||||
network-key-pair:
|
||||
value: AOapcKU6mW5SopFM6eBSiXgbuPJTz11CiEqM+SJGIEOF
|
||||
db-path: sui_config/authorities_db/addeef94d898
|
||||
network-address: /ip4/127.0.0.1/tcp/34633/http
|
||||
json-rpc-address: "127.0.0.1:38025"
|
||||
metrics-address: "127.0.0.1:43451"
|
||||
admin-interface-port: 36793
|
||||
consensus-config:
|
||||
address: /ip4/127.0.0.1/tcp/40307/http
|
||||
db-path: sui_config/consensus_db/addeef94d898
|
||||
internal-worker-address: ~
|
||||
max-pending-transactions: ~
|
||||
narwhal-config:
|
||||
header_num_of_batches_threshold: 32
|
||||
max_header_num_of_batches: 1000
|
||||
max_header_delay: 2000ms
|
||||
min_header_delay: 500ms
|
||||
gc_depth: 50
|
||||
sync_retry_delay: 5000ms
|
||||
sync_retry_nodes: 3
|
||||
batch_size: 500000
|
||||
max_batch_delay: 100ms
|
||||
block_synchronizer:
|
||||
range_synchronize_timeout: 30000ms
|
||||
certificates_synchronize_timeout: 30000ms
|
||||
payload_synchronize_timeout: 30000ms
|
||||
payload_availability_timeout: 30000ms
|
||||
handler_certificate_deliver_timeout: 30000ms
|
||||
consensus_api_grpc:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/37445/http
|
||||
get_collections_timeout: 5000ms
|
||||
remove_collections_timeout: 5000ms
|
||||
max_concurrent_requests: 500000
|
||||
prometheus_metrics:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/43943/http
|
||||
network_admin_server:
|
||||
primary_network_admin_server_port: 39611
|
||||
worker_network_admin_server_base_port: 38377
|
||||
anemo:
|
||||
send_certificate_rate_limit: ~
|
||||
get_payload_availability_rate_limit: ~
|
||||
get_certificates_rate_limit: ~
|
||||
report_batch_rate_limit: ~
|
||||
request_batch_rate_limit: ~
|
||||
enable-event-processing: false
|
||||
enable-index-processing: true
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: 20000000000
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:32965"
|
||||
external-address: /ip4/127.0.0.1/udp/32965
|
||||
genesis:
|
||||
genesis-file-location: sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 2
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 30
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
||||
expensive-safety-check-config:
|
||||
enable-epoch-sui-conservation-check: false
|
||||
enable-deep-per-tx-sui-conservation-check: false
|
||||
force-disable-epoch-sui-conservation-check: false
|
||||
enable-state-consistency-check: false
|
||||
force-disable-state-consistency-check: false
|
||||
enable-move-vm-paranoid-checks: false
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
protocol-key-pair:
|
||||
value: CyNkjqNVr3HrHTH7f/NLs7u5lUHJzuPAw0PqMTD2y2s=
|
||||
worker-key-pair:
|
||||
value: AHd6qvbBv7bTCGGoD1TUR5dOGnwOnYvhHV9ryCUp7rmZ
|
||||
account-key-pair:
|
||||
value: ALSCvWwsVryGIwq+n4f9bIPCRqsooGodE/vDaVCSLfjE
|
||||
network-key-pair:
|
||||
value: APFCK1pRVxn9PDt+KzWx52+EY5nzaZZU2GF9RZoQY58Y
|
||||
db-path: sui_config/authorities_db/b3fd5efb5c87
|
||||
network-address: /ip4/127.0.0.1/tcp/33953/http
|
||||
json-rpc-address: "127.0.0.1:35625"
|
||||
metrics-address: "127.0.0.1:37813"
|
||||
admin-interface-port: 46405
|
||||
consensus-config:
|
||||
address: /ip4/127.0.0.1/tcp/43213/http
|
||||
db-path: sui_config/consensus_db/b3fd5efb5c87
|
||||
internal-worker-address: ~
|
||||
max-pending-transactions: ~
|
||||
narwhal-config:
|
||||
header_num_of_batches_threshold: 32
|
||||
max_header_num_of_batches: 1000
|
||||
max_header_delay: 2000ms
|
||||
min_header_delay: 500ms
|
||||
gc_depth: 50
|
||||
sync_retry_delay: 5000ms
|
||||
sync_retry_nodes: 3
|
||||
batch_size: 500000
|
||||
max_batch_delay: 100ms
|
||||
block_synchronizer:
|
||||
range_synchronize_timeout: 30000ms
|
||||
certificates_synchronize_timeout: 30000ms
|
||||
payload_synchronize_timeout: 30000ms
|
||||
payload_availability_timeout: 30000ms
|
||||
handler_certificate_deliver_timeout: 30000ms
|
||||
consensus_api_grpc:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/46745/http
|
||||
get_collections_timeout: 5000ms
|
||||
remove_collections_timeout: 5000ms
|
||||
max_concurrent_requests: 500000
|
||||
prometheus_metrics:
|
||||
socket_addr: /ip4/127.0.0.1/tcp/38817/http
|
||||
network_admin_server:
|
||||
primary_network_admin_server_port: 34929
|
||||
worker_network_admin_server_base_port: 37447
|
||||
anemo:
|
||||
send_certificate_rate_limit: ~
|
||||
get_payload_availability_rate_limit: ~
|
||||
get_certificates_rate_limit: ~
|
||||
report_batch_rate_limit: ~
|
||||
request_batch_rate_limit: ~
|
||||
enable-event-processing: false
|
||||
enable-index-processing: true
|
||||
grpc-load-shed: ~
|
||||
grpc-concurrency-limit: 20000000000
|
||||
p2p-config:
|
||||
listen-address: "127.0.0.1:39889"
|
||||
external-address: /ip4/127.0.0.1/udp/39889
|
||||
genesis:
|
||||
genesis-file-location: sui_config/genesis.blob
|
||||
authority-store-pruning-config:
|
||||
num-latest-epoch-dbs-to-retain: 3
|
||||
epoch-db-pruning-period-secs: 3600
|
||||
num-epochs-to-retain: 2
|
||||
max-checkpoints-in-batch: 200
|
||||
max-transactions-in-batch: 1000
|
||||
use-range-deletion: true
|
||||
end-of-epoch-broadcast-channel-capacity: 128
|
||||
checkpoint-executor-config:
|
||||
checkpoint-execution-max-concurrency: 200
|
||||
local-execution-timeout-sec: 30
|
||||
db-checkpoint-config:
|
||||
perform-db-checkpoints-at-epoch-end: false
|
||||
indirect-objects-threshold: 18446744073709551615
|
||||
expensive-safety-check-config:
|
||||
enable-epoch-sui-conservation-check: false
|
||||
enable-deep-per-tx-sui-conservation-check: false
|
||||
force-disable-epoch-sui-conservation-check: false
|
||||
enable-state-consistency-check: false
|
||||
force-disable-state-consistency-check: false
|
||||
enable-move-vm-paranoid-checks: false
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["mocha", "chai"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"lib": ["es2020"],
|
||||
"module": "commonjs",
|
||||
"target": "es2020",
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
module watcher
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/ethereum/go-ethereum v1.10.26 // indirect
|
||||
github.com/tidwall/gjson v1.14.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
)
|
|
@ -1,14 +0,0 @@
|
|||
github.com/ethereum/go-ethereum v1.10.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s=
|
||||
github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg=
|
||||
github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
|
||||
github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
@ -1,147 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/tidwall/gjson"
|
||||
"golang.org/x/net/websocket"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
eth_common "github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
||||
type SuiResult struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
TxDigest string `json:"txDigest"`
|
||||
Event struct {
|
||||
MoveEvent struct {
|
||||
PackageID string `json:"packageId"`
|
||||
TransactionModule string `json:"transactionModule"`
|
||||
Sender string `json:"sender"`
|
||||
Type string `json:"type"`
|
||||
Fields *struct {
|
||||
ConsistencyLevel uint8 `json:"consistency_level"`
|
||||
Nonce uint64 `json:"nonce"`
|
||||
Payload string `json:"payload"`
|
||||
Sender uint64 `json:"sender"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
} `json:"fields"`
|
||||
Bcs string `json:"bcs"`
|
||||
} `json:"moveEvent"`
|
||||
} `json:"event"`
|
||||
}
|
||||
|
||||
type SuiEventMsg struct {
|
||||
Jsonrpc string `json:"jsonrpc"`
|
||||
Method *string `json:"method"`
|
||||
ID *int `json:"id"`
|
||||
result *int `json:"result"`
|
||||
Params *struct {
|
||||
Subscription int64 `json:"subscription"`
|
||||
Result *SuiResult `json:"result"`
|
||||
} `json:"params"`
|
||||
}
|
||||
|
||||
type SuiTxnQuery struct {
|
||||
Jsonrpc string `json:"jsonrpc"`
|
||||
Result struct {
|
||||
Data []SuiResult `json:"data"`
|
||||
NextCursor interface{} `json:"nextCursor"`
|
||||
} `json:"result"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
func inspectBody(body gjson.Result) error {
|
||||
txDigest := body.Get("txDigest")
|
||||
timestamp := body.Get("timestamp")
|
||||
packageId := body.Get("event.moveEvent.packageId") // defense in depth: check this
|
||||
account := body.Get("event.moveEvent.sender") // defense in depth: check this
|
||||
consistency_level := body.Get("event.moveEvent.fields.consistency_level")
|
||||
nonce := body.Get("event.moveEvent.fields.nonce")
|
||||
payload := body.Get("event.moveEvent.fields.payload")
|
||||
sender := body.Get("event.moveEvent.fields.sender")
|
||||
sequence := body.Get("event.moveEvent.fields.sequence")
|
||||
|
||||
if !txDigest.Exists() || !timestamp.Exists() || !packageId.Exists() || !account.Exists() || !consistency_level.Exists() || !nonce.Exists() || !payload.Exists() || !sender.Exists() || !sequence.Exists() {
|
||||
return errors.New("block parse error")
|
||||
}
|
||||
|
||||
id, err := base64.StdEncoding.DecodeString(txDigest.String())
|
||||
if err != nil {
|
||||
fmt.Printf("txDigest decode error: %s\n", txDigest.String())
|
||||
return err
|
||||
}
|
||||
|
||||
var txHash = eth_common.BytesToHash(id) // 32 bytes = d3b136a6a182a40554b2fafbc8d12a7a22737c10c81e33b33d1dcb74c532708b
|
||||
fmt.Printf("\ntxHash: %s\n", txHash)
|
||||
|
||||
pl, err := base64.StdEncoding.DecodeString(payload.String())
|
||||
if err != nil {
|
||||
fmt.Printf("payload decode error\n")
|
||||
return err
|
||||
}
|
||||
fmt.Printf("\npl: %s\n", pl)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
origin := "http://localhost/"
|
||||
url := "ws://localhost:9001"
|
||||
ws, err := websocket.Dial(url, "", origin)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
s := fmt.Sprintf(`{"jsonrpc":"2.0", "id": 1, "method": "sui_subscribeEvent", "params": [{"Package": "%s"}]}`, os.Getenv("WORM_PACKAGE"))
|
||||
fmt.Printf("Sending: %s.\n", s)
|
||||
|
||||
if _, err := ws.Write([]byte(s)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for {
|
||||
var msg = make([]byte, 4096)
|
||||
var n int
|
||||
ws.SetReadDeadline(time.Now().Local().Add(1_000_000_000))
|
||||
if n, err = ws.Read(msg); err != nil {
|
||||
fmt.Printf("err")
|
||||
} else {
|
||||
fmt.Printf("\nReceived: %s.\n", msg[:n])
|
||||
parsedMsg := gjson.ParseBytes(msg[:n])
|
||||
|
||||
var res SuiEventMsg
|
||||
err = json.Unmarshal(msg[:n], &res)
|
||||
if err != nil {
|
||||
fmt.Printf("SuiEventMsg: %s", err.Error())
|
||||
}
|
||||
|
||||
if res.Method != nil {
|
||||
fmt.Printf("%s\n", *res.Method)
|
||||
} else {
|
||||
fmt.Printf("Method nil\n")
|
||||
}
|
||||
|
||||
if res.ID != nil {
|
||||
fmt.Printf("%d\n", *res.ID)
|
||||
} else {
|
||||
fmt.Printf("ID nil\n")
|
||||
}
|
||||
|
||||
result := parsedMsg.Get("params.result")
|
||||
if !result.Exists() {
|
||||
// Other messages come through on the channel.. we can ignore them safely
|
||||
continue
|
||||
}
|
||||
fmt.Printf("inspect body called\n")
|
||||
|
||||
err := inspectBody(result)
|
||||
if err != nil {
|
||||
fmt.Printf("inspectBody: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
import websocket
|
||||
import _thread
|
||||
import time
|
||||
import rel
|
||||
import os
|
||||
import pprint
|
||||
import json
|
||||
import base64
|
||||
|
||||
# https://github.com/MystenLabs/sui/pull/5113
|
||||
|
||||
# {
|
||||
# "jsonrpc": "2.0",
|
||||
# "method": "sui_subscribeEvent",
|
||||
# "params": {
|
||||
# "subscription": 1805876586195140,
|
||||
# "result": {
|
||||
# "timestamp": 1666704112752,
|
||||
# "txDigest": "ckB13AaG+OHrO0Ha3I8IK3ERanYHmHAI0jSXnqk9R+I=",
|
||||
# "event": {
|
||||
# "moveEvent": {
|
||||
# "packageId": "0xbd99019f3c8f9d08b5498fedcc97e1c24cddff88",
|
||||
# "transactionModule": "wormhole",
|
||||
# "sender": "0xdc2f7334400a353c6a9303235b578477202809c6",
|
||||
# "type": "0xbd99019f3c8f9d08b5498fedcc97e1c24cddff88::state::WormholeMessage",
|
||||
# "fields": {
|
||||
# "consistency_level": 0,
|
||||
# "nonce": 400,
|
||||
# "payload": "Ag==",
|
||||
# "sender": "0xdc2f7334400a353c6a9303235b578477202809c6",
|
||||
# "sequence": 19,
|
||||
# "timestamp": 0
|
||||
# },
|
||||
# "bcs": "3C9zNEAKNTxqkwMjW1eEdyAoCcYTAAAAAAAAAJABAAAAAAAAAQIAAAAAAAAAAAA="
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# curl -s -X POST -d '{"jsonrpc":"2.0", "id": 1, "method": "sui_getEventsByTransaction", "params": ["KgsiF8pCF61N02zX2oMFYLWQdrbxkOD1ypBxND752No=", 2]}' -H 'Content-Type: application/json' http://127.0.0.1:9000 | jq
|
||||
|
||||
# {
|
||||
# "jsonrpc": "2.0",
|
||||
# "result": [
|
||||
# {
|
||||
# "timestamp": 1666704112752,
|
||||
# "txDigest": "ckB13AaG+OHrO0Ha3I8IK3ERanYHmHAI0jSXnqk9R+I=",
|
||||
# "event": {
|
||||
# "moveEvent": {
|
||||
# "packageId": "0xbd99019f3c8f9d08b5498fedcc97e1c24cddff88",
|
||||
# "transactionModule": "wormhole",
|
||||
# "sender": "0xdc2f7334400a353c6a9303235b578477202809c6",
|
||||
# "type": "0xbd99019f3c8f9d08b5498fedcc97e1c24cddff88::state::WormholeMessage",
|
||||
# "bcs": "3C9zNEAKNTxqkwMjW1eEdyAoCcYTAAAAAAAAAJABAAAAAAAAAQIAAAAAAAAAAAA="
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# ],
|
||||
# "id": 1
|
||||
# }
|
||||
|
||||
def on_message(ws, message):
|
||||
v = json.loads(message)
|
||||
print(json.dumps(v, indent=4))
|
||||
if "params" in v:
|
||||
tx = v["params"]["result"]["txDigest"]
|
||||
#tx = base64.standard_b64decode(tx)
|
||||
print(tx + " -> " + base64.standard_b64decode(tx).hex())
|
||||
|
||||
pl = v["params"]["result"]["event"]["moveEvent"]["fields"]["payload"]
|
||||
pl = base64.standard_b64decode(pl)
|
||||
print(pl.hex())
|
||||
|
||||
def on_error(ws, error):
|
||||
print(error)
|
||||
|
||||
def on_close(ws, close_status_code, close_msg):
|
||||
print("### closed ###")
|
||||
|
||||
def on_open(ws):
|
||||
print("Opened connection")
|
||||
ws.send("{\"jsonrpc\":\"2.0\", \"id\": 1, \"method\": \"sui_subscribeEvent\", \"params\": [{\"Package\": \"" + os.getenv("WORM_PACKAGE") + "\"}]}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
ws = websocket.WebSocketApp("ws://localhost:9001",
|
||||
on_open=on_open,
|
||||
on_message=on_message,
|
||||
on_error=on_error,
|
||||
on_close=on_close)
|
||||
|
||||
ws.run_forever(dispatcher=rel) # Set dispatcher to automatic reconnection
|
||||
rel.signal(2, rel.abort) # Keyboard Interrupt
|
||||
rel.dispatch()
|
|
@ -0,0 +1 @@
|
|||
build
|
|
@ -1,14 +1,18 @@
|
|||
-include ../../Makefile.help
|
||||
|
||||
.PHONY: artifacts
|
||||
artifacts: build
|
||||
VERSION = $(shell grep -Po "version = \"\K[^\"]*" Move.toml | sed "s/\./_/g")
|
||||
|
||||
.PHONY: build
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf build
|
||||
|
||||
.PHONY: check
|
||||
## Build contract
|
||||
build:
|
||||
sui move build
|
||||
check:
|
||||
sui move build -d
|
||||
|
||||
.PHONY: test
|
||||
## Run tests
|
||||
test:
|
||||
sui move test
|
||||
test: check
|
||||
grep "public(friend) fun current_version(): V__${VERSION} {" sources/version_control.move
|
||||
sui move test -t 1
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
[package]
|
||||
name = "TokenBridge"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies.Sui]
|
||||
git = "https://github.com/MystenLabs/sui.git"
|
||||
subdir = "crates/sui-framework/packages/sui-framework"
|
||||
rev = "09b2081498366df936abae26eea4b2d5cafb2788"
|
||||
|
||||
[dependencies.Wormhole]
|
||||
local = "../wormhole"
|
||||
|
||||
[addresses]
|
||||
token_bridge = "_"
|
|
@ -0,0 +1,32 @@
|
|||
# @generated by Move, please check-in and do not edit manually.
|
||||
|
||||
[move]
|
||||
version = 0
|
||||
|
||||
dependencies = [
|
||||
{ name = "Sui" },
|
||||
]
|
||||
|
||||
dev-dependencies = [
|
||||
{ name = "Wormhole" },
|
||||
]
|
||||
|
||||
[[move.package]]
|
||||
name = "MoveStdlib"
|
||||
source = { git = "https://github.com/MystenLabs/sui.git", rev = "09b2081498366df936abae26eea4b2d5cafb2788", subdir = "crates/sui-framework/packages/move-stdlib" }
|
||||
|
||||
[[move.package]]
|
||||
name = "Sui"
|
||||
source = { git = "https://github.com/MystenLabs/sui.git", rev = "09b2081498366df936abae26eea4b2d5cafb2788", subdir = "crates/sui-framework/packages/sui-framework" }
|
||||
|
||||
dependencies = [
|
||||
{ name = "MoveStdlib" },
|
||||
]
|
||||
|
||||
[[move.package]]
|
||||
name = "Wormhole"
|
||||
source = { local = "../wormhole" }
|
||||
|
||||
dependencies = [
|
||||
{ name = "Sui" },
|
||||
]
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "TokenBridge"
|
||||
version = "0.1.1"
|
||||
published-at = "0x4eb7c5bca3759ab3064b46044edb5668c9066be8a543b28b58375f041f876a80"
|
||||
|
||||
[dependencies.Sui]
|
||||
git = "https://github.com/MystenLabs/sui.git"
|
||||
subdir = "crates/sui-framework/packages/sui-framework"
|
||||
rev = "09b2081498366df936abae26eea4b2d5cafb2788"
|
||||
|
||||
[dependencies.Wormhole]
|
||||
local = "../wormhole"
|
||||
|
||||
[addresses]
|
||||
token_bridge = "0x92d81f28c167d90f84638c654b412fe7fa8e55bdfac7f638bdcf70306289be86"
|
|
@ -1,10 +1,21 @@
|
|||
[package]
|
||||
name = "TokenBridge"
|
||||
version = "0.0.1"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework", rev = "2d709054a08d904b9229a2472af679f210af3827" }
|
||||
Wormhole = { local = "../wormhole" }
|
||||
[dependencies.Sui]
|
||||
git = "https://github.com/MystenLabs/sui.git"
|
||||
subdir = "crates/sui-framework/packages/sui-framework"
|
||||
rev = "09b2081498366df936abae26eea4b2d5cafb2788"
|
||||
|
||||
[dependencies.Wormhole]
|
||||
local = "../wormhole"
|
||||
|
||||
[addresses]
|
||||
token_bridge = "0x0"
|
||||
token_bridge = "_"
|
||||
|
||||
[dev-dependencies.Wormhole]
|
||||
local = "../wormhole"
|
||||
|
||||
[dev-addresses]
|
||||
wormhole = "0x100"
|
||||
token_bridge = "0x200"
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
# Sui Wormhole Token Bridge Design
|
||||
|
||||
TODO: make sure this is up to date
|
||||
|
||||
The Token Bridge is responsible for storing treasury caps and locked tokens and exposing functions for initiating and completing transfers, which are gated behind VAAs. It also supports token attestations from foreign chains (which must be done once prior to transfer), contract upgrades, and chain registration.
|
||||
|
||||
## Token Attestation
|
||||
|
||||
TODO: up to date implementation notes
|
||||
|
||||
The sui RPC provides a way to get the object id for CoinMetadata objects:
|
||||
https://github.com/MystenLabs/sui/pull/6281/files#diff-80bf625d87d89549275351d95cfdfab4a6c2a1311804adbc5f1a7fcff225f049R430
|
||||
|
||||
we should document that this will only work for coins whose metadata object is
|
||||
either shared or frozen. This seems to be the case at least for all example
|
||||
coins, so we can probably expect most coins to follow this pattern. Ones that
|
||||
don't, however, will not be transferrable through the token bridge
|
||||
|
||||
## Creating new Coin Types
|
||||
|
||||
TODO: up to date implementation notes
|
||||
|
||||
Internally, `create_wrapped_coin` calls `coin::create_currency<CoinType>(witness, decimals, ctx)`, obtains a treasury cap, and finally stores
|
||||
the treasury cap inside of a `TreasuryCapContainer` object, whose usage is restricted by the functions in its defining module (in particular, gated by VAAs). The `TreasuryCapContainer` is mutably shared so users can access it in a permissionless way. The reason that the treasury cap itself
|
||||
is not mutably shared is that users would be able to use it to mint/burn tokens without limitation.
|
||||
|
||||
## Initiating and Completing Transfers
|
||||
|
||||
The Token Bridge stores both coins transferred by the user for lockup and treasury caps used for minting/burning wrapped assets. To this end, we implement two structs, which are both mutably shared and whose usage is restricted by VAA-gated functions defined in their parent modules.
|
||||
|
||||
```rust
|
||||
struct TreasuryCapContainer<T> {
|
||||
t: TreasuryCap<T>,
|
||||
}
|
||||
```
|
||||
|
||||
```rust
|
||||
struct CoinStore<T> {
|
||||
coins: coin<T>,
|
||||
}
|
||||
```
|
||||
|
||||
Accordingly, we define the following functions for initiating and completing transfers. There is a version of each for wrapped and native coins, because we can't store info about `CoinType` within `BridgeState`. There does not seem to be a way of introspecting the CoinType to determine whether it represents a native or wrapped asset. In addition, we have to use either a `TreasuryCapStore` or `CoinStore` depending on whether we want to initiate or complete a transfer for a native or wrapped asset, which leads to different function signatures.
|
||||
|
||||
### `complete_transfer_wrapped<T>(treasury_cap_store: &mut TreasuryCapStore<T>)`
|
||||
- Use treasury cap to mint wrapped assets to recipient
|
||||
|
||||
### `complete_transfer_native<T>(store: &mut CoinStore<T>)`
|
||||
- Idea is to extract coins from token_bridge and give them to the recipient. We pass in a mutably shared `CoinStore` object, which contains balance or coin objects belonging to token bridge. Coins are extracted from this object and passed to the recipient.
|
||||
|
||||
### `transfer_native<T>(coin: Coin<T>, store: &mut CoinStore<T>)`
|
||||
- Transfer user-supplied native coins to `CoinStore`
|
||||
### `transfer_wrapped<T>(treasury_cap_store: &mut TreasuryCapStore<T>)`
|
||||
- Use the treasury cap to burn some user-supplied wrapped assets
|
||||
|
||||
## Contract Upgrades
|
||||
Not yet supported in Sui.
|
||||
|
||||
## Bridge State
|
||||
TODO: up to date implementation notes
|
|
@ -1,146 +1,385 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
/// This module implements the method `attest_token` which allows someone
|
||||
/// to send asset metadata of a coin type native to Sui. Part of this process
|
||||
/// is registering this asset in the `TokenRegistry`.
|
||||
///
|
||||
/// NOTE: If an asset has not been attested for, it cannot be bridged using
|
||||
/// `transfer_tokens` or `transfer_tokens_with_payload`.
|
||||
///
|
||||
/// See `asset_meta` module for serialization and deserialization of Wormhole
|
||||
/// message payload.
|
||||
module token_bridge::attest_token {
|
||||
use sui::sui::SUI;
|
||||
use sui::coin::{Coin, CoinMetadata};
|
||||
use sui::tx_context::TxContext;
|
||||
use sui::coin::{CoinMetadata};
|
||||
use wormhole::publish_message::{MessageTicket};
|
||||
|
||||
use wormhole::state::{State as WormholeState};
|
||||
use token_bridge::asset_meta::{Self};
|
||||
use token_bridge::create_wrapped::{Self};
|
||||
use token_bridge::state::{Self, State, LatestOnly};
|
||||
use token_bridge::token_registry::{Self};
|
||||
|
||||
use token_bridge::bridge_state::{Self as state, BridgeState};
|
||||
use token_bridge::asset_meta::{Self, AssetMeta};
|
||||
/// Coin type belongs to a wrapped asset.
|
||||
const E_WRAPPED_ASSET: u64 = 0;
|
||||
/// Coin type belongs to an untrusted contract from `create_wrapped` which
|
||||
/// has not completed registration.
|
||||
const E_FROM_CREATE_WRAPPED: u64 = 1;
|
||||
|
||||
public entry fun attest_token<CoinType>(
|
||||
wormhole_state: &mut WormholeState,
|
||||
bridge_state: &mut BridgeState,
|
||||
/// `attest_token` takes `CoinMetadata` of a coin type and generates a
|
||||
/// `MessageTicket` with encoded asset metadata for a foreign Token Bridge
|
||||
/// contract to consume and create a wrapped asset reflecting this Sui
|
||||
/// asset. Asset metadata is encoded using `AssetMeta`.
|
||||
///
|
||||
/// See `token_registry` and `asset_meta` module for more info.
|
||||
public fun attest_token<CoinType>(
|
||||
token_bridge_state: &mut State,
|
||||
coin_meta: &CoinMetadata<CoinType>,
|
||||
fee_coins: Coin<SUI>,
|
||||
ctx: &mut TxContext
|
||||
) {
|
||||
let asset_meta = attest_token_internal(
|
||||
wormhole_state,
|
||||
bridge_state,
|
||||
coin_meta,
|
||||
ctx
|
||||
);
|
||||
let payload = asset_meta::encode(asset_meta);
|
||||
let nonce = 0;
|
||||
state::publish_message(
|
||||
wormhole_state,
|
||||
bridge_state,
|
||||
nonce: u32
|
||||
): MessageTicket {
|
||||
// This capability ensures that the current build version is used.
|
||||
let latest_only = state::assert_latest_only(token_bridge_state);
|
||||
|
||||
// Encode Wormhole message payload.
|
||||
let encoded_asset_meta =
|
||||
serialize_asset_meta(&latest_only, token_bridge_state, coin_meta);
|
||||
|
||||
// Prepare Wormhole message.
|
||||
state::prepare_wormhole_message(
|
||||
&latest_only,
|
||||
token_bridge_state,
|
||||
nonce,
|
||||
payload,
|
||||
fee_coins
|
||||
);
|
||||
encoded_asset_meta
|
||||
)
|
||||
}
|
||||
|
||||
fun attest_token_internal<CoinType>(
|
||||
wormhole_state: &mut WormholeState,
|
||||
bridge_state: &mut BridgeState,
|
||||
fun serialize_asset_meta<CoinType>(
|
||||
latest_only: &LatestOnly,
|
||||
token_bridge_state: &mut State,
|
||||
coin_meta: &CoinMetadata<CoinType>,
|
||||
ctx: &mut TxContext
|
||||
): AssetMeta {
|
||||
let asset_meta =
|
||||
state::register_native_asset<CoinType>(wormhole_state, bridge_state, coin_meta, ctx);
|
||||
return asset_meta
|
||||
): vector<u8> {
|
||||
let registry = state::borrow_token_registry(token_bridge_state);
|
||||
|
||||
// Register if it is a new asset.
|
||||
//
|
||||
// NOTE: We don't want to abort if the asset is already registered
|
||||
// because we may want to send asset metadata again after registration
|
||||
// (the owner of a particular `CoinType` can change `CoinMetadata` any
|
||||
// time after we register the asset).
|
||||
if (token_registry::has<CoinType>(registry)) {
|
||||
let asset_info = token_registry::verified_asset<CoinType>(registry);
|
||||
// If this asset is already registered, there should already
|
||||
// be canonical info associated with this coin type.
|
||||
assert!(
|
||||
!token_registry::is_wrapped(&asset_info),
|
||||
E_WRAPPED_ASSET
|
||||
);
|
||||
} else {
|
||||
// Before we consider registering, we should not accidentally
|
||||
// perform this registration that may be the `CoinMetadata` from
|
||||
// `create_wrapped::prepare_registration`, which has empty fields.
|
||||
assert!(
|
||||
!create_wrapped::incomplete_metadata(coin_meta),
|
||||
E_FROM_CREATE_WRAPPED
|
||||
);
|
||||
|
||||
// Now register it.
|
||||
token_registry::add_new_native(
|
||||
state::borrow_mut_token_registry(
|
||||
latest_only,
|
||||
token_bridge_state
|
||||
),
|
||||
coin_meta
|
||||
);
|
||||
};
|
||||
|
||||
asset_meta::serialize(asset_meta::from_metadata(coin_meta))
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun test_attest_token_internal<CoinType>(
|
||||
wormhole_state: &mut WormholeState,
|
||||
bridge_state: &mut BridgeState,
|
||||
coin_meta: &CoinMetadata<CoinType>,
|
||||
ctx: &mut TxContext
|
||||
): AssetMeta {
|
||||
attest_token_internal<CoinType>(
|
||||
wormhole_state,
|
||||
bridge_state,
|
||||
coin_meta,
|
||||
ctx
|
||||
)
|
||||
public fun serialize_asset_meta_test_only<CoinType>(
|
||||
token_bridge_state: &mut State,
|
||||
coin_metadata: &CoinMetadata<CoinType>,
|
||||
): vector<u8> {
|
||||
// This capability ensures that the current build version is used.
|
||||
let latest_only = state::assert_latest_only(token_bridge_state);
|
||||
|
||||
serialize_asset_meta(&latest_only, token_bridge_state, coin_metadata)
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::attest_token_test{
|
||||
use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_shared, return_shared};
|
||||
use sui::coin::{CoinMetadata};
|
||||
module token_bridge::attest_token_tests {
|
||||
use std::ascii::{Self};
|
||||
use std::string::{Self};
|
||||
use sui::coin::{Self};
|
||||
use sui::test_scenario::{Self};
|
||||
use wormhole::publish_message::{Self};
|
||||
use wormhole::state::{chain_id};
|
||||
|
||||
use wormhole::state::{State};
|
||||
|
||||
use token_bridge::string32::{Self};
|
||||
use token_bridge::bridge_state::{BridgeState};
|
||||
use token_bridge::attest_token::{test_attest_token_internal};
|
||||
use token_bridge::bridge_state_test::{set_up_wormhole_core_and_token_bridges};
|
||||
use token_bridge::native_coin_witness::{Self, NATIVE_COIN_WITNESS};
|
||||
use token_bridge::asset_meta::{Self};
|
||||
|
||||
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
|
||||
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
|
||||
use token_bridge::attest_token::{Self};
|
||||
use token_bridge::coin_native_10::{Self, COIN_NATIVE_10};
|
||||
use token_bridge::coin_wrapped_7::{Self, COIN_WRAPPED_7};
|
||||
use token_bridge::native_asset::{Self};
|
||||
use token_bridge::state::{Self};
|
||||
use token_bridge::token_bridge_scenario::{
|
||||
person,
|
||||
return_state,
|
||||
set_up_wormhole_and_token_bridge,
|
||||
take_state,
|
||||
};
|
||||
use token_bridge::token_registry::{Self};
|
||||
|
||||
#[test]
|
||||
fun test_attest_token(){
|
||||
let test = scenario();
|
||||
let (admin, _, _) = people();
|
||||
fun test_attest_token() {
|
||||
use token_bridge::attest_token::{attest_token};
|
||||
|
||||
test = set_up_wormhole_core_and_token_bridges(admin, test);
|
||||
let user = person();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
next_tx(&mut test, admin); {
|
||||
native_coin_witness::test_init(ctx(&mut test));
|
||||
};
|
||||
next_tx(&mut test, admin); {
|
||||
let wormhole_state = take_shared<State>(&test);
|
||||
let bridge_state = take_shared<BridgeState>(&test);
|
||||
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
|
||||
// Publish coin.
|
||||
coin_native_10::init_test_only(test_scenario::ctx(scenario));
|
||||
|
||||
let asset_meta = test_attest_token_internal<NATIVE_COIN_WITNESS>(
|
||||
&mut wormhole_state,
|
||||
&mut bridge_state,
|
||||
// Set up contracts.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Ignore effects.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
let coin_meta = coin_native_10::take_metadata(scenario);
|
||||
|
||||
// Emit `AssetMeta` payload.
|
||||
let prepared_msg =
|
||||
attest_token(
|
||||
&mut token_bridge_state,
|
||||
&coin_meta,
|
||||
ctx(&mut test)
|
||||
1234, // nonce
|
||||
);
|
||||
|
||||
assert!(asset_meta::get_decimals(&asset_meta)==10, 0);
|
||||
assert!(asset_meta::get_symbol(&asset_meta)==string32::from_bytes(x"00"), 0);
|
||||
assert!(asset_meta::get_name(&asset_meta)==string32::from_bytes(x"11"), 0);
|
||||
// Ignore effects.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
return_shared<State>(wormhole_state);
|
||||
return_shared<BridgeState>(bridge_state);
|
||||
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
|
||||
// Check that asset is registered.
|
||||
{
|
||||
let registry =
|
||||
state::borrow_token_registry(&token_bridge_state);
|
||||
let verified =
|
||||
token_registry::verified_asset<COIN_NATIVE_10>(registry);
|
||||
assert!(!token_registry::is_wrapped(&verified), 0);
|
||||
|
||||
let asset = token_registry::borrow_native<COIN_NATIVE_10>(registry);
|
||||
|
||||
let expected_token_address =
|
||||
native_asset::canonical_address(&coin_meta);
|
||||
assert!(
|
||||
native_asset::token_address(asset) == expected_token_address,
|
||||
0
|
||||
);
|
||||
assert!(native_asset::decimals(asset) == 10, 0);
|
||||
|
||||
let (
|
||||
token_chain,
|
||||
token_address
|
||||
) = native_asset::canonical_info(asset);
|
||||
assert!(token_chain == chain_id(), 0);
|
||||
assert!(token_address == expected_token_address, 0);
|
||||
|
||||
assert!(native_asset::custody(asset) == 0, 0);
|
||||
};
|
||||
test_scenario::end(test);
|
||||
|
||||
// Clean up for next call.
|
||||
publish_message::destroy(prepared_msg);
|
||||
|
||||
// Update metadata.
|
||||
let new_symbol = {
|
||||
use std::vector::{Self};
|
||||
|
||||
let symbol = coin::get_symbol(&coin_meta);
|
||||
let buf = ascii::into_bytes(symbol);
|
||||
vector::reverse(&mut buf);
|
||||
|
||||
ascii::string(buf)
|
||||
};
|
||||
|
||||
let new_name = coin::get_name(&coin_meta);
|
||||
string::append(&mut new_name, string::utf8(b"??? and profit"));
|
||||
|
||||
let treasury_cap = coin_native_10::take_treasury_cap(scenario);
|
||||
coin::update_symbol(&treasury_cap, &mut coin_meta, new_symbol);
|
||||
coin::update_name(&treasury_cap, &mut coin_meta, new_name);
|
||||
|
||||
// We should be able to call `attest_token` any time after.
|
||||
let prepared_msg =
|
||||
attest_token(
|
||||
&mut token_bridge_state,
|
||||
&coin_meta,
|
||||
1234, // nonce
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
publish_message::destroy(prepared_msg);
|
||||
return_state(token_bridge_state);
|
||||
coin_native_10::return_globals(treasury_cap, coin_meta);
|
||||
|
||||
// Done.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = 0, location=0000000000000000000000000000000000000002::dynamic_field)]
|
||||
fun test_attest_token_twice_fails(){
|
||||
let test = scenario();
|
||||
let (admin, _, _) = people();
|
||||
fun test_serialize_asset_meta() {
|
||||
use token_bridge::attest_token::{serialize_asset_meta_test_only};
|
||||
|
||||
test = set_up_wormhole_core_and_token_bridges(admin, test);
|
||||
let user = person();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
next_tx(&mut test, admin); {
|
||||
native_coin_witness::test_init(ctx(&mut test));
|
||||
};
|
||||
next_tx(&mut test, admin); {
|
||||
let wormhole_state = take_shared<State>(&test);
|
||||
let bridge_state = take_shared<BridgeState>(&test);
|
||||
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
|
||||
// Publish coin.
|
||||
coin_native_10::init_test_only(test_scenario::ctx(scenario));
|
||||
|
||||
let _asset_meta_1 = test_attest_token_internal<NATIVE_COIN_WITNESS>(
|
||||
&mut wormhole_state,
|
||||
&mut bridge_state,
|
||||
&coin_meta,
|
||||
ctx(&mut test)
|
||||
// Set up contracts.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Proceed to next operation.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
let coin_meta = coin_native_10::take_metadata(scenario);
|
||||
|
||||
// Emit `AssetMeta` payload.
|
||||
let serialized =
|
||||
serialize_asset_meta_test_only(&mut token_bridge_state, &coin_meta);
|
||||
let expected_serialized =
|
||||
asset_meta::serialize_test_only(
|
||||
asset_meta::from_metadata_test_only(&coin_meta)
|
||||
);
|
||||
let _asset_meta_2 = test_attest_token_internal<NATIVE_COIN_WITNESS>(
|
||||
&mut wormhole_state,
|
||||
&mut bridge_state,
|
||||
&coin_meta,
|
||||
ctx(&mut test)
|
||||
);
|
||||
return_shared<State>(wormhole_state);
|
||||
return_shared<BridgeState>(bridge_state);
|
||||
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
|
||||
assert!(serialized == expected_serialized, 0);
|
||||
|
||||
// Update metadata.
|
||||
let new_symbol = {
|
||||
use std::vector::{Self};
|
||||
|
||||
let symbol = coin::get_symbol(&coin_meta);
|
||||
let buf = ascii::into_bytes(symbol);
|
||||
vector::reverse(&mut buf);
|
||||
|
||||
ascii::string(buf)
|
||||
};
|
||||
test_scenario::end(test);
|
||||
|
||||
let new_name = coin::get_name(&coin_meta);
|
||||
string::append(&mut new_name, string::utf8(b"??? and profit"));
|
||||
|
||||
let treasury_cap = coin_native_10::take_treasury_cap(scenario);
|
||||
coin::update_symbol(&treasury_cap, &mut coin_meta, new_symbol);
|
||||
coin::update_name(&treasury_cap, &mut coin_meta, new_name);
|
||||
|
||||
// Check that the new serialization reflects updated metadata.
|
||||
let expected_serialized =
|
||||
asset_meta::serialize_test_only(
|
||||
asset_meta::from_metadata_test_only(&coin_meta)
|
||||
);
|
||||
assert!(serialized != expected_serialized, 0);
|
||||
let updated_serialized =
|
||||
serialize_asset_meta_test_only(&mut token_bridge_state, &coin_meta);
|
||||
assert!(updated_serialized == expected_serialized, 0);
|
||||
|
||||
// Clean up.
|
||||
return_state(token_bridge_state);
|
||||
coin_native_10::return_globals(treasury_cap, coin_meta);
|
||||
|
||||
// Done.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = attest_token::E_FROM_CREATE_WRAPPED)]
|
||||
fun test_cannot_attest_token_from_create_wrapped() {
|
||||
use token_bridge::attest_token::{attest_token};
|
||||
|
||||
let user = person();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Publish coin.
|
||||
coin_wrapped_7::init_test_only(test_scenario::ctx(scenario));
|
||||
|
||||
// Ignore effects.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
// Set up contracts.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Ignore effects.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
let coin_meta = test_scenario::take_shared(scenario);
|
||||
|
||||
// You shall not pass!
|
||||
let prepared_msg =
|
||||
attest_token<COIN_WRAPPED_7>(
|
||||
&mut token_bridge_state,
|
||||
&coin_meta,
|
||||
1234 // nonce
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
publish_message::destroy(prepared_msg);
|
||||
|
||||
abort 42
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = wormhole::package_utils::E_NOT_CURRENT_VERSION)]
|
||||
fun test_cannot_attest_token_outdated_version() {
|
||||
use token_bridge::attest_token::{attest_token};
|
||||
|
||||
let user = person();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Publish coin.
|
||||
coin_wrapped_7::init_test_only(test_scenario::ctx(scenario));
|
||||
|
||||
// Ignore effects.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
// Set up contracts.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Ignore effects.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
let coin_meta = test_scenario::take_shared(scenario);
|
||||
|
||||
// Conveniently roll version back.
|
||||
state::reverse_migrate_version(&mut token_bridge_state);
|
||||
|
||||
// Simulate executing with an outdated build by upticking the minimum
|
||||
// required version for `publish_message` to something greater than
|
||||
// this build.
|
||||
state::migrate_version_test_only(
|
||||
&mut token_bridge_state,
|
||||
token_bridge::version_control::previous_version_test_only(),
|
||||
token_bridge::version_control::next_version()
|
||||
);
|
||||
|
||||
// You shall not pass!
|
||||
let prepared_msg =
|
||||
attest_token<COIN_WRAPPED_7>(
|
||||
&mut token_bridge_state,
|
||||
&coin_meta,
|
||||
1234 // nonce
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
publish_message::destroy(prepared_msg);
|
||||
|
||||
abort 42
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,510 +0,0 @@
|
|||
module token_bridge::bridge_state {
|
||||
use std::option::{Self, Option};
|
||||
use std::ascii::{Self};
|
||||
|
||||
use sui::object::{Self, UID};
|
||||
use sui::vec_map::{Self, VecMap};
|
||||
use sui::tx_context::{TxContext};
|
||||
use sui::coin::{Self, Coin, TreasuryCap, CoinMetadata};
|
||||
use sui::transfer::{Self};
|
||||
use sui::tx_context::{Self};
|
||||
use sui::sui::SUI;
|
||||
|
||||
use token_bridge::string32;
|
||||
use token_bridge::dynamic_set;
|
||||
use token_bridge::asset_meta::{Self, AssetMeta};
|
||||
|
||||
use wormhole::external_address::{Self, ExternalAddress};
|
||||
use wormhole::myu16::{U16};
|
||||
use wormhole::wormhole::{Self};
|
||||
use wormhole::state::{Self as wormhole_state, State as WormholeState};
|
||||
use wormhole::emitter::{EmitterCapability};
|
||||
use wormhole::set::{Self, Set};
|
||||
|
||||
const E_IS_NOT_WRAPPED_ASSET: u64 = 0;
|
||||
const E_IS_NOT_REGISTERED_NATIVE_ASSET: u64 = 1;
|
||||
const E_COIN_TYPE_HAS_NO_REGISTERED_INTEGER_ADDRESS: u64 = 2;
|
||||
const E_COIN_TYPE_HAS_REGISTERED_INTEGER_ADDRESS: u64 = 3;
|
||||
const E_ORIGIN_CHAIN_MISMATCH: u64 = 4;
|
||||
const E_ORIGIN_ADDRESS_MISMATCH: u64 = 5;
|
||||
const E_IS_WRAPPED_ASSET: u64 = 6;
|
||||
|
||||
friend token_bridge::vaa;
|
||||
friend token_bridge::register_chain;
|
||||
friend token_bridge::wrapped;
|
||||
friend token_bridge::complete_transfer;
|
||||
friend token_bridge::transfer_tokens;
|
||||
friend token_bridge::attest_token;
|
||||
#[test_only]
|
||||
friend token_bridge::bridge_state_test;
|
||||
#[test_only]
|
||||
friend token_bridge::complete_transfer_test;
|
||||
#[test_only]
|
||||
friend token_bridge::token_bridge_vaa_test;
|
||||
|
||||
/// Capability for creating a bridge state object, granted to sender when this
|
||||
/// module is deployed
|
||||
struct DeployerCapability has key, store {id: UID}
|
||||
|
||||
/// WrappedAssetInfo<CoinType> stores all the metadata about a wrapped asset
|
||||
struct WrappedAssetInfo<phantom CoinType> has key, store {
|
||||
id: UID,
|
||||
token_chain: U16,
|
||||
token_address: ExternalAddress,
|
||||
treasury_cap: TreasuryCap<CoinType>,
|
||||
}
|
||||
|
||||
struct NativeAssetInfo<phantom CoinType> has key, store {
|
||||
id: UID,
|
||||
// Even though we can look up token_chain at any time from wormhole state,
|
||||
// it can be more efficient to store it here locally so we don't have to do lookups.
|
||||
custody: Coin<CoinType>,
|
||||
asset_meta: AssetMeta,
|
||||
}
|
||||
|
||||
/// OriginInfo is a non-Sui object that stores info about a tokens native token
|
||||
/// chain and address
|
||||
struct OriginInfo<phantom CoinType> has store, copy, drop {
|
||||
token_chain: U16,
|
||||
token_address: ExternalAddress,
|
||||
}
|
||||
|
||||
public fun get_token_chain_from_origin_info<CoinType>(origin_info: &OriginInfo<CoinType>): U16 {
|
||||
return origin_info.token_chain
|
||||
}
|
||||
|
||||
public fun get_token_address_from_origin_info<CoinType>(origin_info: &OriginInfo<CoinType>): ExternalAddress {
|
||||
return origin_info.token_address
|
||||
}
|
||||
|
||||
public fun get_origin_info_from_wrapped_asset_info<CoinType>(wrapped_asset_info: &WrappedAssetInfo<CoinType>): OriginInfo<CoinType> {
|
||||
OriginInfo { token_chain: wrapped_asset_info.token_chain, token_address: wrapped_asset_info.token_address }
|
||||
}
|
||||
|
||||
public fun get_origin_info_from_native_asset_info<CoinType>(native_asset_info: &NativeAssetInfo<CoinType>): OriginInfo<CoinType> {
|
||||
let asset_meta = &native_asset_info.asset_meta;
|
||||
let token_chain = asset_meta::get_token_chain(asset_meta);
|
||||
let token_address = asset_meta::get_token_address(asset_meta);
|
||||
OriginInfo { token_chain, token_address }
|
||||
}
|
||||
|
||||
public(friend) fun create_wrapped_asset_info<CoinType>(
|
||||
token_chain: U16,
|
||||
token_address: ExternalAddress,
|
||||
treasury_cap: TreasuryCap<CoinType>,
|
||||
ctx: &mut TxContext
|
||||
): WrappedAssetInfo<CoinType> {
|
||||
return WrappedAssetInfo {
|
||||
id: object::new(ctx),
|
||||
token_chain,
|
||||
token_address,
|
||||
treasury_cap
|
||||
}
|
||||
}
|
||||
|
||||
// Integer label for coin types registered with Wormhole
|
||||
|
||||
struct NativeIdRegistry has key, store {
|
||||
id: UID,
|
||||
index: u64, // next index to use
|
||||
}
|
||||
|
||||
fun next_native_id(registry: &mut NativeIdRegistry): ExternalAddress {
|
||||
use wormhole::serialize::serialize_u64;
|
||||
|
||||
let cur_index = registry.index;
|
||||
registry.index = cur_index + 1;
|
||||
let bytes = std::vector::empty<u8>();
|
||||
serialize_u64(&mut bytes, cur_index);
|
||||
external_address::from_bytes(bytes)
|
||||
}
|
||||
|
||||
// Treasury caps, token stores, consumed VAAs, registered emitters, etc.
|
||||
// are stored as dynamic fields of bridge state.
|
||||
struct BridgeState has key, store {
|
||||
id: UID,
|
||||
|
||||
/// Set of consumed VAA hashes
|
||||
consumed_vaas: Set<vector<u8>>,
|
||||
|
||||
/// Token bridge owned emitter capability
|
||||
emitter_cap: EmitterCapability,
|
||||
|
||||
/// Mapping of bridge contracts on other chains
|
||||
registered_emitters: VecMap<U16, ExternalAddress>,
|
||||
|
||||
native_id_registry: NativeIdRegistry,
|
||||
}
|
||||
|
||||
fun init(ctx: &mut TxContext) {
|
||||
transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun test_init(ctx: &mut TxContext) {
|
||||
transfer::transfer(DeployerCapability{id: object::new(ctx)}, tx_context::sender(ctx));
|
||||
}
|
||||
|
||||
// converts owned state object into a shared object, so that anyone can get a reference to &mut State
|
||||
// and pass it into various functions
|
||||
public entry fun init_and_share_state(
|
||||
deployer: DeployerCapability,
|
||||
emitter_cap: EmitterCapability,
|
||||
ctx: &mut TxContext
|
||||
) {
|
||||
let DeployerCapability{ id } = deployer;
|
||||
object::delete(id);
|
||||
let state = BridgeState {
|
||||
id: object::new(ctx),
|
||||
consumed_vaas: set::new(ctx),
|
||||
emitter_cap,
|
||||
registered_emitters: vec_map::empty(),
|
||||
native_id_registry: NativeIdRegistry {
|
||||
id: object::new(ctx),
|
||||
index: 1,
|
||||
}
|
||||
};
|
||||
|
||||
// permanently shares state
|
||||
transfer::share_object(state);
|
||||
}
|
||||
|
||||
public(friend) fun deposit<CoinType>(
|
||||
bridge_state: &mut BridgeState,
|
||||
coin: Coin<CoinType>,
|
||||
) {
|
||||
// TODO: create custom errors for each dynamic_set::borrow_mut
|
||||
let native_asset = dynamic_set::borrow_mut<NativeAssetInfo<CoinType>>(&mut bridge_state.id);
|
||||
coin::join<CoinType>(&mut native_asset.custody, coin);
|
||||
}
|
||||
|
||||
public(friend) fun withdraw<CoinType>(
|
||||
_verified_coin_witness: VerifiedCoinType<CoinType>,
|
||||
bridge_state: &mut BridgeState,
|
||||
value: u64,
|
||||
ctx: &mut TxContext
|
||||
): Coin<CoinType> {
|
||||
let native_asset = dynamic_set::borrow_mut<NativeAssetInfo<CoinType>>(&mut bridge_state.id);
|
||||
coin::split<CoinType>(&mut native_asset.custody, value, ctx)
|
||||
}
|
||||
|
||||
public(friend) fun mint<CoinType>(
|
||||
_verified_coin_witness: VerifiedCoinType<CoinType>,
|
||||
bridge_state: &mut BridgeState,
|
||||
value: u64,
|
||||
ctx: &mut TxContext,
|
||||
): Coin<CoinType> {
|
||||
let wrapped_info = dynamic_set::borrow_mut<WrappedAssetInfo<CoinType>>(&mut bridge_state.id);
|
||||
coin::mint<CoinType>(&mut wrapped_info.treasury_cap, value, ctx)
|
||||
}
|
||||
|
||||
public(friend) fun burn<CoinType>(
|
||||
bridge_state: &mut BridgeState,
|
||||
coin: Coin<CoinType>,
|
||||
) {
|
||||
let wrapped_info = dynamic_set::borrow_mut<WrappedAssetInfo<CoinType>>(&mut bridge_state.id);
|
||||
coin::burn<CoinType>(&mut wrapped_info.treasury_cap, coin);
|
||||
}
|
||||
|
||||
public(friend) fun publish_message(
|
||||
wormhole_state: &mut WormholeState,
|
||||
bridge_state: &mut BridgeState,
|
||||
nonce: u64,
|
||||
payload: vector<u8>,
|
||||
message_fee: Coin<SUI>,
|
||||
): u64 {
|
||||
wormhole::publish_message(
|
||||
&mut bridge_state.emitter_cap,
|
||||
wormhole_state,
|
||||
nonce,
|
||||
payload,
|
||||
message_fee,
|
||||
)
|
||||
}
|
||||
|
||||
/// getters
|
||||
|
||||
public fun vaa_is_consumed(state: &BridgeState, hash: vector<u8>): bool {
|
||||
set::contains(&state.consumed_vaas, hash)
|
||||
}
|
||||
|
||||
public fun get_registered_emitter(state: &BridgeState, chain_id: &U16): Option<ExternalAddress> {
|
||||
if (vec_map::contains(&state.registered_emitters, chain_id)) {
|
||||
option::some(*vec_map::get(&state.registered_emitters, chain_id))
|
||||
} else {
|
||||
option::none()
|
||||
}
|
||||
}
|
||||
|
||||
public fun is_wrapped_asset<CoinType>(bridge_state: &BridgeState): bool {
|
||||
dynamic_set::exists_<WrappedAssetInfo<CoinType>>(&bridge_state.id)
|
||||
}
|
||||
|
||||
public fun is_registered_native_asset<CoinType>(bridge_state: &BridgeState): bool {
|
||||
dynamic_set::exists_<NativeAssetInfo<CoinType>>(&bridge_state.id)
|
||||
}
|
||||
|
||||
/// Returns the origin information for a CoinType
|
||||
public fun origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
|
||||
if (is_wrapped_asset<CoinType>(bridge_state)) {
|
||||
get_wrapped_asset_origin_info<CoinType>(bridge_state)
|
||||
} else {
|
||||
get_registered_native_asset_origin_info(bridge_state)
|
||||
}
|
||||
}
|
||||
|
||||
/// A value of type `VerifiedCoinType<T>` witnesses the fact that the type
|
||||
/// `T` has been verified to correspond to a particular chain id and token
|
||||
/// address (may be either a wrapped or native asset).
|
||||
/// The verification is performed by `verify_coin_type`.
|
||||
///
|
||||
/// This is important because the coin type is an input to several
|
||||
/// functions, and is thus untrusted. Most coin-related functionality
|
||||
/// requires passing in a coin type generic argument.
|
||||
/// When transferring tokens *out*, that type instantiation determines the
|
||||
/// token bridge's behaviour, and thus we just take whatever was supplied.
|
||||
/// When transferring tokens *in*, it's the transfer VAA that determines
|
||||
/// which coin should be used via the origin chain and origin address
|
||||
/// fields.
|
||||
///
|
||||
/// For technical reasons, the latter case still requires a type argument to
|
||||
/// be passed in (since Move does not support existential types, so we must
|
||||
/// rely on old school universal quantification). We must thus verify that
|
||||
/// the supplied type corresponds to the origin info in the VAA.
|
||||
///
|
||||
/// Accordingly, the `mint` and `withdraw` operations are gated by this
|
||||
/// witness type, since these two operations require a VAA to supply the
|
||||
/// token information. This ensures that those two functions can't be called
|
||||
/// without first verifying the `CoinType`.
|
||||
struct VerifiedCoinType<phantom CoinType> has copy, drop {}
|
||||
|
||||
/// See the documentation for `VerifiedCoinType` above.
|
||||
public fun verify_coin_type<CoinType>(
|
||||
bridge_state: &BridgeState,
|
||||
token_chain: U16,
|
||||
token_address: ExternalAddress
|
||||
): VerifiedCoinType<CoinType> {
|
||||
let coin_origin = origin_info<CoinType>(bridge_state);
|
||||
assert!(coin_origin.token_chain == token_chain, E_ORIGIN_CHAIN_MISMATCH);
|
||||
assert!(coin_origin.token_address == token_address, E_ORIGIN_ADDRESS_MISMATCH);
|
||||
VerifiedCoinType {}
|
||||
}
|
||||
|
||||
public fun get_wrapped_asset_origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
|
||||
assert!(is_wrapped_asset<CoinType>(bridge_state), E_IS_NOT_WRAPPED_ASSET);
|
||||
let wrapped_asset_info = dynamic_set::borrow<WrappedAssetInfo<CoinType>>(&bridge_state.id);
|
||||
get_origin_info_from_wrapped_asset_info(wrapped_asset_info)
|
||||
}
|
||||
|
||||
public fun get_registered_native_asset_origin_info<CoinType>(bridge_state: &BridgeState): OriginInfo<CoinType> {
|
||||
let native_asset_info = dynamic_set::borrow<NativeAssetInfo<CoinType>>(&bridge_state.id);
|
||||
get_origin_info_from_native_asset_info(native_asset_info)
|
||||
}
|
||||
|
||||
/// setters
|
||||
|
||||
public(friend) fun set_registered_emitter(state: &mut BridgeState, chain_id: U16, emitter: ExternalAddress) {
|
||||
if (vec_map::contains<U16, ExternalAddress>(&mut state.registered_emitters, &chain_id)){
|
||||
vec_map::remove<U16, ExternalAddress>(&mut state.registered_emitters, &chain_id);
|
||||
};
|
||||
vec_map::insert<U16, ExternalAddress>(&mut state.registered_emitters, chain_id, emitter);
|
||||
}
|
||||
|
||||
/// dynamic ops
|
||||
|
||||
public(friend) fun store_consumed_vaa(bridge_state: &mut BridgeState, vaa: vector<u8>) {
|
||||
set::add(&mut bridge_state.consumed_vaas, vaa);
|
||||
}
|
||||
|
||||
public(friend) fun register_wrapped_asset<CoinType>(bridge_state: &mut BridgeState, wrapped_asset_info: WrappedAssetInfo<CoinType>) {
|
||||
dynamic_set::add<WrappedAssetInfo<CoinType>>(&mut bridge_state.id, wrapped_asset_info);
|
||||
}
|
||||
|
||||
public(friend) fun register_native_asset<CoinType>(
|
||||
wormhole_state: &WormholeState,
|
||||
bridge_state: &mut BridgeState,
|
||||
coin_meta: &CoinMetadata<CoinType>,
|
||||
ctx: &mut TxContext
|
||||
): AssetMeta {
|
||||
assert!(!is_wrapped_asset<CoinType>(bridge_state), E_IS_WRAPPED_ASSET); // TODO - test
|
||||
let asset_meta = asset_meta::create(
|
||||
next_native_id(&mut bridge_state.native_id_registry),
|
||||
wormhole_state::get_chain_id(wormhole_state), // TODO: should we just hardcode this?
|
||||
coin::get_decimals<CoinType>(coin_meta), // decimals
|
||||
string32::from_bytes(ascii::into_bytes(coin::get_symbol<CoinType>(coin_meta))), // symbol
|
||||
string32::from_string(&coin::get_name<CoinType>(coin_meta)) // name
|
||||
);
|
||||
let native_asset_info = NativeAssetInfo<CoinType> {
|
||||
id: object::new(ctx),
|
||||
custody: coin::zero(ctx),
|
||||
asset_meta,
|
||||
};
|
||||
dynamic_set::add<NativeAssetInfo<CoinType>>(&mut bridge_state.id, native_asset_info);
|
||||
asset_meta
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::bridge_state_test{
|
||||
use sui::test_scenario::{Self, Scenario, next_tx, ctx, take_from_address, take_shared, return_shared};
|
||||
use sui::coin::{CoinMetadata};
|
||||
|
||||
use wormhole::state::{State};
|
||||
use wormhole::test_state::{init_wormhole_state};
|
||||
use wormhole::wormhole::{Self};
|
||||
use wormhole::external_address::{Self};
|
||||
|
||||
use token_bridge::bridge_state::{Self as state, BridgeState, DeployerCapability};
|
||||
use token_bridge::native_coin_witness::{Self, NATIVE_COIN_WITNESS};
|
||||
use token_bridge::native_coin_witness_v2::{Self, NATIVE_COIN_WITNESS_V2};
|
||||
|
||||
fun scenario(): Scenario { test_scenario::begin(@0x123233) }
|
||||
fun people(): (address, address, address) { (@0x124323, @0xE05, @0xFACE) }
|
||||
|
||||
#[test]
|
||||
fun test_state_setters() {
|
||||
test_state_setters_(scenario())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fun test_coin_type_addressing(){
|
||||
test_coin_type_addressing_(scenario())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = 0, location=0000000000000000000000000000000000000002::dynamic_field)]
|
||||
fun test_coin_type_addressing_failure_case(){
|
||||
test_coin_type_addressing_failure_case_(scenario())
|
||||
}
|
||||
|
||||
public fun set_up_wormhole_core_and_token_bridges(admin: address, test: Scenario): Scenario {
|
||||
// init and share wormhole core bridge
|
||||
test = init_wormhole_state(test, admin);
|
||||
|
||||
// call init for token bridge to get deployer cap
|
||||
next_tx(&mut test, admin); {
|
||||
state::test_init(ctx(&mut test));
|
||||
};
|
||||
|
||||
// register for emitter cap and init_and_share token bridge
|
||||
next_tx(&mut test, admin); {
|
||||
let wormhole_state = take_shared<State>(&test);
|
||||
let my_emitter = wormhole::register_emitter(&mut wormhole_state, ctx(&mut test));
|
||||
let deployer = take_from_address<DeployerCapability>(&test, admin);
|
||||
state::init_and_share_state(deployer, my_emitter, ctx(&mut test));
|
||||
return_shared<State>(wormhole_state);
|
||||
};
|
||||
|
||||
next_tx(&mut test, admin); {
|
||||
let bridge_state = take_shared<BridgeState>(&test);
|
||||
return_shared<BridgeState>(bridge_state);
|
||||
};
|
||||
|
||||
return test
|
||||
}
|
||||
|
||||
fun test_state_setters_(test: Scenario) {
|
||||
let (admin, _, _) = people();
|
||||
|
||||
test = set_up_wormhole_core_and_token_bridges(admin, test);
|
||||
|
||||
//test BridgeState setter and getter functions
|
||||
next_tx(&mut test, admin); {
|
||||
let state = take_shared<BridgeState>(&test);
|
||||
|
||||
// test store consumed vaa
|
||||
state::store_consumed_vaa(&mut state, x"1234");
|
||||
assert!(state::vaa_is_consumed(&state, x"1234"), 0);
|
||||
|
||||
// TODO - test store coin store
|
||||
// TODO - test store treasury cap
|
||||
|
||||
return_shared<BridgeState>(state);
|
||||
};
|
||||
test_scenario::end(test);
|
||||
}
|
||||
|
||||
fun test_coin_type_addressing_(test: Scenario) {
|
||||
let (admin, _, _) = people();
|
||||
|
||||
test = set_up_wormhole_core_and_token_bridges(admin, test);
|
||||
|
||||
//test coin type addressing
|
||||
next_tx(&mut test, admin); {
|
||||
native_coin_witness::test_init(ctx(&mut test));
|
||||
native_coin_witness_v2::test_init(ctx(&mut test));
|
||||
};
|
||||
next_tx(&mut test, admin); {
|
||||
let wormhole_state = take_shared<State>(&test);
|
||||
let bridge_state = take_shared<BridgeState>(&test);
|
||||
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
|
||||
state::register_native_asset<NATIVE_COIN_WITNESS>(
|
||||
&mut wormhole_state,
|
||||
&mut bridge_state,
|
||||
&coin_meta,
|
||||
ctx(&mut test)
|
||||
);
|
||||
let origin_info = state::origin_info<NATIVE_COIN_WITNESS>(&bridge_state);
|
||||
let address = state::get_token_address_from_origin_info(&origin_info);
|
||||
assert!(address == external_address::from_bytes(x"01"), 0);
|
||||
|
||||
let coin_meta_v2 = take_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(&test);
|
||||
state::register_native_asset<NATIVE_COIN_WITNESS_V2>(
|
||||
&mut wormhole_state,
|
||||
&mut bridge_state,
|
||||
&coin_meta_v2,
|
||||
ctx(&mut test)
|
||||
);
|
||||
let origin_info = state::origin_info<NATIVE_COIN_WITNESS_V2>(&bridge_state);
|
||||
let address = state::get_token_address_from_origin_info(&origin_info);
|
||||
assert!(address == external_address::from_bytes(x"02"), 0);
|
||||
|
||||
return_shared<State>(wormhole_state);
|
||||
return_shared<BridgeState>(bridge_state);
|
||||
return_shared<CoinMetadata<NATIVE_COIN_WITNESS_V2>>(coin_meta_v2);
|
||||
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
|
||||
};
|
||||
test_scenario::end(test);
|
||||
}
|
||||
|
||||
|
||||
fun test_coin_type_addressing_failure_case_(test: Scenario) {
|
||||
let (admin, _, _) = people();
|
||||
|
||||
test = set_up_wormhole_core_and_token_bridges(admin, test);
|
||||
|
||||
//test coin type addressing
|
||||
next_tx(&mut test, admin); {
|
||||
native_coin_witness::test_init(ctx(&mut test));
|
||||
native_coin_witness_v2::test_init(ctx(&mut test));
|
||||
};
|
||||
next_tx(&mut test, admin); {
|
||||
let wormhole_state = take_shared<State>(&test);
|
||||
let bridge_state = take_shared<BridgeState>(&test);
|
||||
let coin_meta = take_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(&test);
|
||||
state::register_native_asset<NATIVE_COIN_WITNESS>(
|
||||
&mut wormhole_state,
|
||||
&mut bridge_state,
|
||||
&coin_meta,
|
||||
ctx(&mut test)
|
||||
);
|
||||
let origin_info = state::origin_info<NATIVE_COIN_WITNESS>(&bridge_state);
|
||||
let address = state::get_token_address_from_origin_info(&origin_info);
|
||||
assert!(address == external_address::from_bytes(x"01"), 0);
|
||||
|
||||
// aborts because trying to re-register native coin
|
||||
state::register_native_asset<NATIVE_COIN_WITNESS>(
|
||||
&mut wormhole_state,
|
||||
&mut bridge_state,
|
||||
&coin_meta,
|
||||
ctx(&mut test)
|
||||
);
|
||||
|
||||
return_shared<State>(wormhole_state);
|
||||
return_shared<BridgeState>(bridge_state);
|
||||
return_shared<CoinMetadata<NATIVE_COIN_WITNESS>>(coin_meta);
|
||||
};
|
||||
test_scenario::end(test);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,775 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
/// This module implements two methods: `authorize_transfer` and `redeem_coin`,
|
||||
/// which are to be executed in a transaction block in this order.
|
||||
///
|
||||
/// `authorize_transfer` allows a contract to complete a Token Bridge transfer
|
||||
/// with arbitrary payload. This deserialized `TransferWithPayload` with the
|
||||
/// bridged balance and source chain ID are packaged in a `RedeemerReceipt`.
|
||||
///
|
||||
/// `redeem_coin` unpacks the `RedeemerReceipt` and checks whether the specified
|
||||
/// `EmitterCap` is the specified redeemer for this transfer. If he is the
|
||||
/// correct redeemer, the balance is unpacked and transformed into `Coin` and
|
||||
/// is returned alongside `TransferWithPayload` and source chain ID.
|
||||
///
|
||||
/// The purpose of splitting this transfer redemption into two steps is in case
|
||||
/// Token Bridge needs to be upgraded and there is a breaking change for this
|
||||
/// module, an integrator would not be left broken. It is discouraged to put
|
||||
/// `authorize_transfer` in an integrator's package logic. Otherwise, this
|
||||
/// integrator needs to be prepared to upgrade his contract to handle the latest
|
||||
/// version of `complete_transfer_with_payload`.
|
||||
///
|
||||
/// Instead, an integrator is encouraged to execute a transaction block, which
|
||||
/// executes `authorize_transfer` using the latest Token Bridge package ID and
|
||||
/// to implement `redeem_coin` in his contract to consume this receipt. This is
|
||||
/// similar to how an integrator with Wormhole is not meant to use
|
||||
/// `vaa::parse_and_verify` in his contract in case the `vaa` module needs to
|
||||
/// be upgraded due to a breaking change.
|
||||
///
|
||||
/// Like in `complete_transfer`, a VAA with an encoded transfer can be redeemed
|
||||
/// only once.
|
||||
///
|
||||
/// See `transfer_with_payload` module for serialization and deserialization of
|
||||
/// Wormhole message payload.
|
||||
module token_bridge::complete_transfer_with_payload {
|
||||
use sui::coin::{Self, Coin};
|
||||
use sui::object::{Self};
|
||||
use sui::tx_context::{TxContext};
|
||||
use wormhole::emitter::{EmitterCap};
|
||||
|
||||
use token_bridge::complete_transfer::{Self};
|
||||
use token_bridge::state::{Self, State, LatestOnly};
|
||||
use token_bridge::transfer_with_payload::{Self, TransferWithPayload};
|
||||
use token_bridge::vaa::{Self, TokenBridgeMessage};
|
||||
|
||||
/// `EmitterCap` address does not agree with encoded redeemer.
|
||||
const E_INVALID_REDEEMER: u64 = 0;
|
||||
|
||||
/// This type is only generated from `authorize_transfer` and can only be
|
||||
/// redeemed using `redeem_coin`. Integrators are expected to implement
|
||||
/// `redeem_coin` within their contracts and call `authorize_transfer` in a
|
||||
/// transaction block preceding the method that consumes this receipt. The
|
||||
/// only way to destroy this receipt is callling `redeem_coin` with an
|
||||
/// `EmitterCap` generated from the `wormhole::emitter` module, whose ID is
|
||||
/// the expected redeemer for this token transfer.
|
||||
struct RedeemerReceipt<phantom CoinType> {
|
||||
/// Which chain ID this transfer originated from.
|
||||
source_chain: u16,
|
||||
/// Deserialized transfer info.
|
||||
parsed: TransferWithPayload,
|
||||
/// Coin of bridged asset.
|
||||
bridged_out: Coin<CoinType>
|
||||
}
|
||||
|
||||
/// `authorize_transfer` deserializes a token transfer VAA payload, which
|
||||
/// encodes its own arbitrary payload (which has meaning to the redeemer).
|
||||
/// Once the transfer is authorized, an event (`TransferRedeemed`) is
|
||||
/// emitted to reflect which Token Bridge this transfer originated from.
|
||||
/// The `RedeemerReceipt` returned wraps a balance reflecting the encoded
|
||||
/// transfer amount along with the source chain and deserialized
|
||||
/// `TransferWithPayload`.
|
||||
///
|
||||
/// NOTE: This method is guarded by a minimum build version check. This
|
||||
/// method could break backward compatibility on an upgrade.
|
||||
///
|
||||
/// It is important for integrators to refrain from calling this method
|
||||
/// within their contracts. This method is meant to be called in a
|
||||
/// transaction block, passing the `RedeemerReceipt` to a method which calls
|
||||
/// `redeem_coin` within a contract. If in a circumstance where this module
|
||||
/// has a breaking change in an upgrade, `redeem_coin` will not be affected
|
||||
/// by this change.
|
||||
///
|
||||
/// See `redeem_coin` for more details.
|
||||
public fun authorize_transfer<CoinType>(
|
||||
token_bridge_state: &mut State,
|
||||
msg: TokenBridgeMessage,
|
||||
ctx: &mut TxContext
|
||||
): RedeemerReceipt<CoinType> {
|
||||
// This capability ensures that the current build version is used.
|
||||
let latest_only = state::assert_latest_only(token_bridge_state);
|
||||
|
||||
// Emitting the transfer being redeemed.
|
||||
//
|
||||
// NOTE: We save the emitter chain ID to save the integrator from
|
||||
// having to `parse_and_verify` the same encoded VAA to get this info.
|
||||
let source_chain =
|
||||
complete_transfer::emit_transfer_redeemed(&msg);
|
||||
|
||||
// Finally deserialize the Wormhole message payload and handle bridging
|
||||
// out token of a given coin type.
|
||||
handle_authorize_transfer(
|
||||
&latest_only,
|
||||
token_bridge_state,
|
||||
source_chain,
|
||||
vaa::take_payload(msg),
|
||||
ctx
|
||||
)
|
||||
}
|
||||
|
||||
/// After a transfer is authorized, only a valid redeemer may unpack the
|
||||
/// `RedeemerReceipt`. The specified `EmitterCap` is the only authorized
|
||||
/// redeemer of the transfer. Once the redeemer is validated, coin from
|
||||
/// this receipt of the specified coin type is returned alongside the
|
||||
/// deserialized `TransferWithPayload` and source chain ID.
|
||||
///
|
||||
/// NOTE: Integrators of Token Bridge redeeming these token transfers should
|
||||
/// be calling only this method from their contracts. This method is not
|
||||
/// guarded by version control (thus not requiring a reference to the
|
||||
/// Token Bridge `State` object), so it is intended to work for any package
|
||||
/// version.
|
||||
public fun redeem_coin<CoinType>(
|
||||
emitter_cap: &EmitterCap,
|
||||
receipt: RedeemerReceipt<CoinType>
|
||||
): (
|
||||
Coin<CoinType>,
|
||||
TransferWithPayload,
|
||||
u16 // `wormhole::vaa::emitter_chain`
|
||||
) {
|
||||
let RedeemerReceipt { source_chain, parsed, bridged_out } = receipt;
|
||||
|
||||
// Transfer must be redeemed by the contract's registered Wormhole
|
||||
// emitter.
|
||||
let redeemer = transfer_with_payload::redeemer_id(&parsed);
|
||||
assert!(redeemer == object::id(emitter_cap), E_INVALID_REDEEMER);
|
||||
|
||||
// Create coin from balance and return other unpacked members of receipt.
|
||||
(bridged_out, parsed, source_chain)
|
||||
}
|
||||
|
||||
fun handle_authorize_transfer<CoinType>(
|
||||
latest_only: &LatestOnly,
|
||||
token_bridge_state: &mut State,
|
||||
source_chain: u16,
|
||||
transfer_vaa_payload: vector<u8>,
|
||||
ctx: &mut TxContext
|
||||
): RedeemerReceipt<CoinType> {
|
||||
// Deserialize for processing.
|
||||
let parsed = transfer_with_payload::deserialize(transfer_vaa_payload);
|
||||
|
||||
// Handle bridging assets out to be returned to method caller.
|
||||
//
|
||||
// See `complete_transfer` module for more info.
|
||||
let (
|
||||
_,
|
||||
bridged_out,
|
||||
) =
|
||||
complete_transfer::verify_and_bridge_out(
|
||||
latest_only,
|
||||
token_bridge_state,
|
||||
transfer_with_payload::token_chain(&parsed),
|
||||
transfer_with_payload::token_address(&parsed),
|
||||
transfer_with_payload::redeemer_chain(&parsed),
|
||||
transfer_with_payload::amount(&parsed)
|
||||
);
|
||||
|
||||
RedeemerReceipt {
|
||||
source_chain,
|
||||
parsed,
|
||||
bridged_out: coin::from_balance(bridged_out, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun burn<CoinType>(receipt: RedeemerReceipt<CoinType>) {
|
||||
let RedeemerReceipt {
|
||||
source_chain: _,
|
||||
parsed: _,
|
||||
bridged_out
|
||||
} = receipt;
|
||||
coin::burn_for_testing(bridged_out);
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::complete_transfer_with_payload_tests {
|
||||
use sui::coin::{Self};
|
||||
use sui::object::{Self};
|
||||
use sui::test_scenario::{Self};
|
||||
use wormhole::emitter::{Self};
|
||||
use wormhole::state::{chain_id};
|
||||
use wormhole::wormhole_scenario::{new_emitter, parse_and_verify_vaa};
|
||||
|
||||
use token_bridge::coin_wrapped_12::{Self, COIN_WRAPPED_12};
|
||||
use token_bridge::complete_transfer_with_payload::{Self};
|
||||
use token_bridge::complete_transfer::{Self};
|
||||
use token_bridge::coin_native_10::{Self, COIN_NATIVE_10};
|
||||
use token_bridge::dummy_message::{Self};
|
||||
use token_bridge::native_asset::{Self};
|
||||
use token_bridge::state::{Self};
|
||||
use token_bridge::token_bridge_scenario::{
|
||||
register_dummy_emitter,
|
||||
return_state,
|
||||
set_up_wormhole_and_token_bridge,
|
||||
take_state,
|
||||
two_people
|
||||
};
|
||||
use token_bridge::token_registry::{Self};
|
||||
use token_bridge::transfer_with_payload::{Self};
|
||||
use token_bridge::vaa::{Self};
|
||||
use token_bridge::wrapped_asset::{Self};
|
||||
|
||||
#[test]
|
||||
/// Test the public-facing function authorize_transfer.
|
||||
/// using a native transfer VAA_ATTESTED_DECIMALS_10.
|
||||
fun test_complete_transfer_with_payload_native_asset() {
|
||||
use token_bridge::complete_transfer_with_payload::{
|
||||
authorize_transfer,
|
||||
redeem_coin
|
||||
};
|
||||
|
||||
let transfer_vaa =
|
||||
dummy_message::encoded_transfer_with_payload_vaa_native();
|
||||
|
||||
let (user, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Initialize Wormhole and Token Bridge.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register Sui as a foreign emitter.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Initialize native token.
|
||||
let mint_amount = 1000000;
|
||||
coin_native_10::init_register_and_deposit(
|
||||
scenario,
|
||||
coin_deployer,
|
||||
mint_amount
|
||||
);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
{
|
||||
let asset = token_registry::borrow_native<COIN_NATIVE_10>(
|
||||
state::borrow_token_registry(&token_bridge_state)
|
||||
);
|
||||
assert!(native_asset::custody(asset) == mint_amount, 0);
|
||||
};
|
||||
|
||||
// Set up dummy `EmitterCap` as the expected redeemer.
|
||||
let emitter_cap = emitter::dummy();
|
||||
|
||||
// Verify that the emitter cap is the expected redeemer.
|
||||
let expected_transfer =
|
||||
transfer_with_payload::deserialize(
|
||||
wormhole::vaa::take_payload(
|
||||
parse_and_verify_vaa(scenario, transfer_vaa)
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
transfer_with_payload::redeemer_id(&expected_transfer) == object::id(&emitter_cap),
|
||||
0
|
||||
);
|
||||
|
||||
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
// Execute authorize_transfer.
|
||||
let receipt =
|
||||
authorize_transfer<COIN_NATIVE_10>(
|
||||
&mut token_bridge_state,
|
||||
msg,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
let (
|
||||
bridged,
|
||||
parsed_transfer,
|
||||
source_chain
|
||||
) = redeem_coin(&emitter_cap, receipt);
|
||||
|
||||
assert!(source_chain == expected_source_chain, 0);
|
||||
|
||||
// Assert coin value, source chain, and parsed transfer details are correct.
|
||||
// We expect the coin value to be 300000, because that's in terms of
|
||||
// 10 decimals. The amount specifed in the VAA_ATTESTED_DECIMALS_12 is 3000, because that's
|
||||
// in terms of 8 decimals.
|
||||
let expected_bridged = 300000;
|
||||
assert!(coin::value(&bridged) == expected_bridged, 0);
|
||||
|
||||
// Amount left on custody should be whatever is left remaining after
|
||||
// the transfer.
|
||||
let remaining = mint_amount - expected_bridged;
|
||||
{
|
||||
let asset = token_registry::borrow_native<COIN_NATIVE_10>(
|
||||
state::borrow_token_registry(&token_bridge_state)
|
||||
);
|
||||
assert!(native_asset::custody(asset) == remaining, 0);
|
||||
};
|
||||
|
||||
// Verify token info.
|
||||
let registry = state::borrow_token_registry(&token_bridge_state);
|
||||
let verified =
|
||||
token_registry::verified_asset<COIN_NATIVE_10>(registry);
|
||||
let expected_token_chain = token_registry::token_chain(&verified);
|
||||
let expected_token_address = token_registry::token_address(&verified);
|
||||
assert!(expected_token_chain == chain_id(), 0);
|
||||
assert!(
|
||||
transfer_with_payload::token_chain(&parsed_transfer) == expected_token_chain,
|
||||
0
|
||||
);
|
||||
assert!(
|
||||
transfer_with_payload::token_address(&parsed_transfer) == expected_token_address,
|
||||
0
|
||||
);
|
||||
|
||||
// Verify transfer by serializing both parsed and expected.
|
||||
let serialized = transfer_with_payload::serialize(parsed_transfer);
|
||||
let expected_serialized =
|
||||
transfer_with_payload::serialize(expected_transfer);
|
||||
assert!(serialized == expected_serialized, 0);
|
||||
|
||||
// Clean up.
|
||||
return_state(token_bridge_state);
|
||||
coin::burn_for_testing(bridged);
|
||||
emitter::destroy_test_only(emitter_cap);
|
||||
|
||||
// Done.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Test the public-facing functions `authorize_transfer` and `redeem_coin`.
|
||||
/// Use an actual devnet Wormhole complete transfer with payload
|
||||
/// VAA_ATTESTED_DECIMALS_12.
|
||||
///
|
||||
/// This test confirms that:
|
||||
/// - `authorize_transfer` with `redeem_coin` deserializes the encoded
|
||||
/// transfer and recovers the source chain, payload, and additional
|
||||
/// transfer details wrapped in a redeemer receipt.
|
||||
/// - a wrapped coin with the correct value is minted by the bridge
|
||||
/// and returned by authorize_transfer
|
||||
///
|
||||
fun test_complete_transfer_with_payload_wrapped_asset() {
|
||||
use token_bridge::complete_transfer_with_payload::{
|
||||
authorize_transfer,
|
||||
redeem_coin
|
||||
};
|
||||
|
||||
let transfer_vaa =
|
||||
dummy_message::encoded_transfer_with_payload_wrapped_12();
|
||||
|
||||
let (user, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Initialize Wormhole and Token Bridge.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register chain ID 2 as a foreign emitter.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Register wrapped token.
|
||||
coin_wrapped_12::init_and_register(scenario, coin_deployer);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
// Set up dummy `EmitterCap` as the expected redeemer.
|
||||
let emitter_cap = emitter::dummy();
|
||||
|
||||
// Verify that the emitter cap is the expected redeemer.
|
||||
let expected_transfer =
|
||||
transfer_with_payload::deserialize(
|
||||
wormhole::vaa::take_payload(
|
||||
parse_and_verify_vaa(scenario, transfer_vaa)
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
transfer_with_payload::redeemer_id(&expected_transfer) == object::id(&emitter_cap),
|
||||
0
|
||||
);
|
||||
|
||||
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
// Execute authorize_transfer.
|
||||
let receipt =
|
||||
authorize_transfer<COIN_WRAPPED_12>(
|
||||
&mut token_bridge_state,
|
||||
msg,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
let (
|
||||
bridged,
|
||||
parsed_transfer,
|
||||
source_chain
|
||||
) = redeem_coin(&emitter_cap, receipt);
|
||||
assert!(source_chain == expected_source_chain, 0);
|
||||
|
||||
// Assert coin value, source chain, and parsed transfer details are correct.
|
||||
let expected_bridged = 3000;
|
||||
assert!(coin::value(&bridged) == expected_bridged, 0);
|
||||
|
||||
// Total supply should equal the amount just minted.
|
||||
let registry = state::borrow_token_registry(&token_bridge_state);
|
||||
{
|
||||
let asset =
|
||||
token_registry::borrow_wrapped<COIN_WRAPPED_12>(registry);
|
||||
assert!(wrapped_asset::total_supply(asset) == expected_bridged, 0);
|
||||
};
|
||||
|
||||
// Verify token info.
|
||||
let verified =
|
||||
token_registry::verified_asset<COIN_WRAPPED_12>(registry);
|
||||
let expected_token_chain = token_registry::token_chain(&verified);
|
||||
let expected_token_address = token_registry::token_address(&verified);
|
||||
assert!(expected_token_chain != chain_id(), 0);
|
||||
assert!(
|
||||
transfer_with_payload::token_chain(&parsed_transfer) == expected_token_chain,
|
||||
0
|
||||
);
|
||||
assert!(
|
||||
transfer_with_payload::token_address(&parsed_transfer) == expected_token_address,
|
||||
0
|
||||
);
|
||||
|
||||
// Verify transfer by serializing both parsed and expected.
|
||||
let serialized = transfer_with_payload::serialize(parsed_transfer);
|
||||
let expected_serialized =
|
||||
transfer_with_payload::serialize(expected_transfer);
|
||||
assert!(serialized == expected_serialized, 0);
|
||||
|
||||
// Clean up.
|
||||
return_state(token_bridge_state);
|
||||
coin::burn_for_testing(bridged);
|
||||
emitter::destroy_test_only(emitter_cap);
|
||||
|
||||
// Done.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(
|
||||
abort_code = complete_transfer_with_payload::E_INVALID_REDEEMER,
|
||||
)]
|
||||
/// Test the public-facing function authorize_transfer.
|
||||
/// This test fails because the ecmitter_cap (recipient) is incorrect (0x2 instead of 0x3).
|
||||
///
|
||||
fun test_cannot_complete_transfer_with_payload_invalid_redeemer() {
|
||||
use token_bridge::complete_transfer_with_payload::{
|
||||
authorize_transfer,
|
||||
redeem_coin
|
||||
};
|
||||
|
||||
let transfer_vaa =
|
||||
dummy_message::encoded_transfer_with_payload_wrapped_12();
|
||||
|
||||
let (user, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Initialize Wormhole and Token Bridge.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register chain ID 2 as a foreign emitter.
|
||||
register_dummy_emitter(scenario, 2);
|
||||
|
||||
// Register wrapped asset with 12 decimals.
|
||||
coin_wrapped_12::init_and_register(scenario, coin_deployer);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
let parsed =
|
||||
transfer_with_payload::deserialize(
|
||||
wormhole::vaa::take_payload(
|
||||
parse_and_verify_vaa(scenario, transfer_vaa)
|
||||
)
|
||||
);
|
||||
|
||||
// Because the vaa expects the dummy emitter as the redeemer, we need
|
||||
// to generate another emitter.
|
||||
let emitter_cap = new_emitter(scenario);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
assert!(
|
||||
transfer_with_payload::redeemer_id(&parsed) != object::id(&emitter_cap),
|
||||
0
|
||||
);
|
||||
|
||||
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let receipt =
|
||||
authorize_transfer<COIN_WRAPPED_12>(
|
||||
&mut token_bridge_state,
|
||||
msg,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
// You shall not pass!
|
||||
let (
|
||||
bridged_out,
|
||||
_,
|
||||
_
|
||||
) = redeem_coin(&emitter_cap, receipt);
|
||||
|
||||
// Clean up.
|
||||
coin::burn_for_testing(bridged_out);
|
||||
|
||||
abort 42
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(
|
||||
abort_code = complete_transfer::E_CANONICAL_TOKEN_INFO_MISMATCH
|
||||
)]
|
||||
/// This test demonstrates that the `CoinType` specified for the token
|
||||
/// redemption must agree with the canonical token info encoded in the VAA_ATTESTED_DECIMALS_12,
|
||||
/// which is registered with the Token Bridge.
|
||||
fun test_cannot_complete_transfer_with_payload_wrong_coin_type() {
|
||||
use token_bridge::complete_transfer_with_payload::{
|
||||
authorize_transfer
|
||||
};
|
||||
|
||||
let transfer_vaa =
|
||||
dummy_message::encoded_transfer_with_payload_wrapped_12();
|
||||
|
||||
let (user, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Initialize Wormhole and Token Bridge.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register chain ID 2 as a foreign emitter.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Register wrapped token.
|
||||
coin_wrapped_12::init_and_register(scenario, coin_deployer);
|
||||
|
||||
// Also register unexpected token (in this case a native one).
|
||||
coin_native_10::init_and_register(scenario, coin_deployer);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
let registry = state::borrow_token_registry(&token_bridge_state);
|
||||
|
||||
// Set up dummy `EmitterCap` as the expected redeemer.
|
||||
let emitter_cap = emitter::dummy();
|
||||
|
||||
// Verify that the emitter cap is the expected redeemer.
|
||||
let expected_transfer =
|
||||
transfer_with_payload::deserialize(
|
||||
wormhole::vaa::take_payload(
|
||||
parse_and_verify_vaa(scenario, transfer_vaa)
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
transfer_with_payload::redeemer_id(&expected_transfer) == object::id(&emitter_cap),
|
||||
0
|
||||
);
|
||||
|
||||
// Also verify that the encoded token info disagrees with the expected
|
||||
// token info.
|
||||
let verified =
|
||||
token_registry::verified_asset<COIN_NATIVE_10>(registry);
|
||||
let expected_token_chain = token_registry::token_chain(&verified);
|
||||
let expected_token_address = token_registry::token_address(&verified);
|
||||
assert!(
|
||||
transfer_with_payload::token_chain(&expected_transfer) != expected_token_chain,
|
||||
0
|
||||
);
|
||||
assert!(
|
||||
transfer_with_payload::token_address(&expected_transfer) != expected_token_address,
|
||||
0
|
||||
);
|
||||
|
||||
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
// You shall not pass!
|
||||
let receipt =
|
||||
authorize_transfer<COIN_NATIVE_10>(
|
||||
&mut token_bridge_state,
|
||||
msg,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
return_state(token_bridge_state);
|
||||
complete_transfer_with_payload::burn(receipt);
|
||||
emitter::destroy_test_only(emitter_cap);
|
||||
|
||||
// Done.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = complete_transfer::E_TARGET_NOT_SUI)]
|
||||
/// This test verifies that `complete_transfer` reverts when a transfer is
|
||||
/// sent to the wrong target blockchain (chain ID != 21).
|
||||
fun test_cannot_complete_transfer_with_payload_wrapped_asset_invalid_target_chain() {
|
||||
use token_bridge::complete_transfer_with_payload::{
|
||||
authorize_transfer
|
||||
};
|
||||
|
||||
let transfer_vaa =
|
||||
dummy_message::encoded_transfer_with_payload_wrapped_12_invalid_target_chain();
|
||||
|
||||
let (user, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Initialize Wormhole and Token Bridge.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register chain ID 2 as a foreign emitter.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Register wrapped token.
|
||||
coin_wrapped_12::init_and_register(scenario, coin_deployer);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
// Set up dummy `EmitterCap` as the expected redeemer.
|
||||
let emitter_cap = emitter::dummy();
|
||||
|
||||
// Verify that the emitter cap is the expected redeemer.
|
||||
let expected_transfer =
|
||||
transfer_with_payload::deserialize(
|
||||
wormhole::vaa::take_payload(
|
||||
parse_and_verify_vaa(scenario, transfer_vaa)
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
transfer_with_payload::redeemer_id(&expected_transfer) == object::id(&emitter_cap),
|
||||
0
|
||||
);
|
||||
|
||||
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
// You shall not pass!
|
||||
let receipt =
|
||||
authorize_transfer<COIN_WRAPPED_12>(
|
||||
&mut token_bridge_state,
|
||||
msg,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
complete_transfer_with_payload::burn(receipt);
|
||||
|
||||
abort 42
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = wormhole::package_utils::E_NOT_CURRENT_VERSION)]
|
||||
fun test_cannot_complete_transfer_with_payload_outdated_version() {
|
||||
use token_bridge::complete_transfer_with_payload::{authorize_transfer};
|
||||
|
||||
let transfer_vaa =
|
||||
dummy_message::encoded_transfer_with_payload_vaa_native();
|
||||
|
||||
let (user, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(user);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Initialize Wormhole and Token Bridge.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register Sui as a foreign emitter.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Initialize native token.
|
||||
let mint_amount = 1000000;
|
||||
coin_native_10::init_register_and_deposit(
|
||||
scenario,
|
||||
coin_deployer,
|
||||
mint_amount
|
||||
);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
// Set up dummy `EmitterCap` as the expected redeemer.
|
||||
let emitter_cap = emitter::dummy();
|
||||
|
||||
// Verify that the emitter cap is the expected redeemer.
|
||||
let expected_transfer =
|
||||
transfer_with_payload::deserialize(
|
||||
wormhole::vaa::take_payload(
|
||||
parse_and_verify_vaa(scenario, transfer_vaa)
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
transfer_with_payload::redeemer_id(&expected_transfer) == object::id(&emitter_cap),
|
||||
0
|
||||
);
|
||||
|
||||
let verified_vaa = parse_and_verify_vaa(scenario, transfer_vaa);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
// Ignore effects. Begin processing as arbitrary tx executor.
|
||||
test_scenario::next_tx(scenario, user);
|
||||
|
||||
// Conveniently roll version back.
|
||||
state::reverse_migrate_version(&mut token_bridge_state);
|
||||
|
||||
// Simulate executing with an outdated build by upticking the minimum
|
||||
// required version for `publish_message` to something greater than
|
||||
// this build.
|
||||
state::migrate_version_test_only(
|
||||
&mut token_bridge_state,
|
||||
token_bridge::version_control::previous_version_test_only(),
|
||||
token_bridge::version_control::next_version()
|
||||
);
|
||||
|
||||
// You shall not pass!
|
||||
let receipt =
|
||||
authorize_transfer<COIN_NATIVE_10>(
|
||||
&mut token_bridge_state,
|
||||
msg,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
complete_transfer_with_payload::burn(receipt);
|
||||
|
||||
abort 42
|
||||
}
|
||||
}
|
|
@ -0,0 +1,642 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
/// This module implements methods that create a specific coin type reflecting a
|
||||
/// wrapped (foreign) asset, whose metadata is encoded in a VAA sent from
|
||||
/// another network.
|
||||
///
|
||||
/// Wrapped assets are created in two steps.
|
||||
/// 1. `prepare_registration`: This method creates a new `TreasuryCap` for a
|
||||
/// given coin type and wraps an encoded asset metadata VAA. We require a
|
||||
/// one-time witness (OTW) to throw an explicit error (even though it is
|
||||
/// redundant with what `create_currency` requires). This coin will
|
||||
/// be published using this method, meaning the `init` method in that
|
||||
/// untrusted package will have the asset's decimals hard-coded for its
|
||||
/// coin metadata. A `WrappedAssetSetup` object is transferred to the
|
||||
/// transaction sender.
|
||||
/// 2. `complete_registration`: This method destroys the `WrappedAssetSetup`
|
||||
/// object by unpacking its `TreasuryCap`, which will be warehoused in the
|
||||
/// `TokenRegistry`. The shared coin metadata object will be updated to
|
||||
/// reflect the contents of the encoded asset metadata payload.
|
||||
///
|
||||
/// Wrapped asset metadata can also be updated with a new asset metadata VAA.
|
||||
/// By calling `update_attestation`, Token Bridge verifies that the specific
|
||||
/// coin type is registered and agrees with the encoded asset metadata's
|
||||
/// canonical token info. `ForeignInfo` and the coin's metadata will be updated
|
||||
/// based on the encoded asset metadata payload.
|
||||
///
|
||||
/// See `state` and `wrapped_asset` modules for more details.
|
||||
///
|
||||
/// References:
|
||||
/// https://examples.sui.io/basics/one-time-witness.html
|
||||
module token_bridge::create_wrapped {
|
||||
use std::ascii::{Self};
|
||||
use std::option::{Self};
|
||||
use std::type_name::{Self};
|
||||
use sui::coin::{Self, TreasuryCap, CoinMetadata};
|
||||
use sui::object::{Self, UID};
|
||||
use sui::package::{UpgradeCap};
|
||||
use sui::transfer::{Self};
|
||||
use sui::tx_context::{TxContext};
|
||||
|
||||
use token_bridge::asset_meta::{Self};
|
||||
use token_bridge::normalized_amount::{max_decimals};
|
||||
use token_bridge::state::{Self, State};
|
||||
use token_bridge::token_registry::{Self};
|
||||
use token_bridge::vaa::{Self, TokenBridgeMessage};
|
||||
use token_bridge::wrapped_asset::{Self};
|
||||
|
||||
#[test_only]
|
||||
use token_bridge::version_control::{Self, V__0_2_0 as V__CURRENT};
|
||||
|
||||
/// Failed one-time witness verification.
|
||||
const E_BAD_WITNESS: u64 = 0;
|
||||
/// Coin witness does not equal "COIN".
|
||||
const E_INVALID_COIN_MODULE_NAME: u64 = 1;
|
||||
/// Decimals value exceeds `MAX_DECIMALS` from `normalized_amount`.
|
||||
const E_DECIMALS_EXCEED_WRAPPED_MAX: u64 = 2;
|
||||
|
||||
/// A.K.A. "coin".
|
||||
const COIN_MODULE_NAME: vector<u8> = b"coin";
|
||||
|
||||
/// Container holding new coin type's `TreasuryCap` and encoded asset metadata
|
||||
/// VAA, which are required to complete this asset's registration.
|
||||
struct WrappedAssetSetup<phantom CoinType, phantom Version> has key, store {
|
||||
id: UID,
|
||||
treasury_cap: TreasuryCap<CoinType>
|
||||
}
|
||||
|
||||
/// This method is executed within the `init` method of an untrusted module,
|
||||
/// which defines a one-time witness (OTW) type (`CoinType`). OTW is
|
||||
/// required to ensure that only one `TreasuryCap` exists for `CoinType`. This
|
||||
/// is similar to how a `TreasuryCap` is created in `coin::create_currency`.
|
||||
///
|
||||
/// Because this method is stateless (i.e. no dependency on Token Bridge's
|
||||
/// `State` object), the contract defers VAA verification to
|
||||
/// `complete_registration` after this method has been executed.
|
||||
public fun prepare_registration<CoinType: drop, Version>(
|
||||
witness: CoinType,
|
||||
decimals: u8,
|
||||
ctx: &mut TxContext
|
||||
): WrappedAssetSetup<CoinType, Version> {
|
||||
let setup = prepare_registration_internal(witness, decimals, ctx);
|
||||
|
||||
// Also make sure that this witness module name is literally "coin".
|
||||
let module_name = type_name::get_module(&type_name::get<CoinType>());
|
||||
assert!(
|
||||
ascii::into_bytes(module_name) == COIN_MODULE_NAME,
|
||||
E_INVALID_COIN_MODULE_NAME
|
||||
);
|
||||
|
||||
setup
|
||||
}
|
||||
|
||||
/// This function performs the bulk of `prepare_registration`, except
|
||||
/// checking the module name. This separation is useful for testing.
|
||||
fun prepare_registration_internal<CoinType: drop, Version>(
|
||||
witness: CoinType,
|
||||
decimals: u8,
|
||||
ctx: &mut TxContext
|
||||
): WrappedAssetSetup<CoinType, Version> {
|
||||
// Make sure there's only one instance of the type `CoinType`. This
|
||||
// resembles the same check for `coin::create_currency`.
|
||||
// Technically this check is redundant as it's performed by
|
||||
// `coin::create_currency` below, but it doesn't hurt.
|
||||
assert!(sui::types::is_one_time_witness(&witness), E_BAD_WITNESS);
|
||||
|
||||
// Ensure that the decimals passed into this method do not exceed max
|
||||
// decimals (see `normalized_amount` module).
|
||||
assert!(decimals <= max_decimals(), E_DECIMALS_EXCEED_WRAPPED_MAX);
|
||||
|
||||
// We initialise the currency with empty metadata. Later on, in the
|
||||
// `complete_registration` call, when `CoinType` gets associated with a
|
||||
// VAA, we update these fields.
|
||||
let no_symbol = b"";
|
||||
let no_name = b"";
|
||||
let no_description = b"";
|
||||
let no_icon_url = option::none();
|
||||
|
||||
let (treasury_cap, coin_meta) =
|
||||
coin::create_currency(
|
||||
witness,
|
||||
decimals,
|
||||
no_symbol,
|
||||
no_name,
|
||||
no_description,
|
||||
no_icon_url,
|
||||
ctx
|
||||
);
|
||||
|
||||
// The CoinMetadata is turned into a shared object so that other
|
||||
// functions (and wallets) can easily grab references to it. This is
|
||||
// safe to do, as the metadata setters require a `TreasuryCap` for the
|
||||
// coin too, which is held by the token bridge.
|
||||
transfer::public_share_object(coin_meta);
|
||||
|
||||
// Create `WrappedAssetSetup` object and transfer to transaction sender.
|
||||
// The owner of this object will call `complete_registration` to destroy
|
||||
// it.
|
||||
WrappedAssetSetup {
|
||||
id: object::new(ctx),
|
||||
treasury_cap
|
||||
}
|
||||
}
|
||||
|
||||
/// After executing `prepare_registration`, owner of `WrappedAssetSetup`
|
||||
/// executes this method to complete this wrapped asset's registration.
|
||||
///
|
||||
/// This method destroys `WrappedAssetSetup`, unpacking the `TreasuryCap` and
|
||||
/// encoded asset metadata VAA. The deserialized asset metadata VAA is used
|
||||
/// to update the associated `CoinMetadata`.
|
||||
public fun complete_registration<CoinType: drop, Version>(
|
||||
token_bridge_state: &mut State,
|
||||
coin_meta: &mut CoinMetadata<CoinType>,
|
||||
setup: WrappedAssetSetup<CoinType, Version>,
|
||||
coin_upgrade_cap: UpgradeCap,
|
||||
msg: TokenBridgeMessage
|
||||
) {
|
||||
// This capability ensures that the current build version is used. This
|
||||
// call performs an additional check of whether `WrappedAssetSetup` was
|
||||
// created using the current package.
|
||||
let latest_only =
|
||||
state::assert_latest_only_specified<Version>(token_bridge_state);
|
||||
|
||||
let WrappedAssetSetup {
|
||||
id,
|
||||
treasury_cap
|
||||
} = setup;
|
||||
|
||||
// Finally destroy the object.
|
||||
object::delete(id);
|
||||
|
||||
// Deserialize to `AssetMeta`.
|
||||
let token_meta = asset_meta::deserialize(vaa::take_payload(msg));
|
||||
|
||||
// `register_wrapped_asset` uses `token_registry::add_new_wrapped`,
|
||||
// which will check whether the asset has already been registered and if
|
||||
// the token chain ID is not Sui's.
|
||||
//
|
||||
// If both of these conditions are met, `register_wrapped_asset` will
|
||||
// succeed and the new wrapped coin will be registered.
|
||||
token_registry::add_new_wrapped(
|
||||
state::borrow_mut_token_registry(&latest_only, token_bridge_state),
|
||||
token_meta,
|
||||
coin_meta,
|
||||
treasury_cap,
|
||||
coin_upgrade_cap
|
||||
);
|
||||
}
|
||||
|
||||
/// For registered wrapped assets, we can update `ForeignInfo` for a
|
||||
/// given `CoinType` with a new asset meta VAA emitted from another network.
|
||||
public fun update_attestation<CoinType>(
|
||||
token_bridge_state: &mut State,
|
||||
coin_meta: &mut CoinMetadata<CoinType>,
|
||||
msg: TokenBridgeMessage
|
||||
) {
|
||||
// This capability ensures that the current build version is used.
|
||||
let latest_only = state::assert_latest_only(token_bridge_state);
|
||||
|
||||
// Deserialize to `AssetMeta`.
|
||||
let token_meta = asset_meta::deserialize(vaa::take_payload(msg));
|
||||
|
||||
// This asset must exist in the registry.
|
||||
let registry =
|
||||
state::borrow_mut_token_registry(&latest_only, token_bridge_state);
|
||||
token_registry::assert_has<CoinType>(registry);
|
||||
|
||||
// Now update wrapped.
|
||||
wrapped_asset::update_metadata(
|
||||
token_registry::borrow_mut_wrapped<CoinType>(registry),
|
||||
coin_meta,
|
||||
token_meta
|
||||
);
|
||||
}
|
||||
|
||||
public fun incomplete_metadata<CoinType>(
|
||||
coin_meta: &CoinMetadata<CoinType>
|
||||
): bool {
|
||||
use std::string::{bytes};
|
||||
use std::vector::{is_empty};
|
||||
|
||||
(
|
||||
is_empty(ascii::as_bytes(&coin::get_symbol(coin_meta))) &&
|
||||
is_empty(bytes(&coin::get_name(coin_meta))) &&
|
||||
is_empty(bytes(&coin::get_description(coin_meta))) &&
|
||||
std::option::is_none(&coin::get_icon_url(coin_meta))
|
||||
)
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun new_setup_test_only<CoinType: drop, Version: drop>(
|
||||
_version: Version,
|
||||
witness: CoinType,
|
||||
decimals: u8,
|
||||
ctx: &mut TxContext
|
||||
): (WrappedAssetSetup<CoinType, Version>, UpgradeCap) {
|
||||
let setup =
|
||||
prepare_registration_internal(
|
||||
witness,
|
||||
decimals,
|
||||
ctx
|
||||
);
|
||||
|
||||
let upgrade_cap =
|
||||
sui::package::test_publish(
|
||||
object::id_from_address(@token_bridge),
|
||||
ctx
|
||||
);
|
||||
|
||||
(setup, upgrade_cap)
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun new_setup_current<CoinType: drop>(
|
||||
witness: CoinType,
|
||||
decimals: u8,
|
||||
ctx: &mut TxContext
|
||||
): (WrappedAssetSetup<CoinType, V__CURRENT>, UpgradeCap) {
|
||||
new_setup_test_only(
|
||||
version_control::current_version_test_only(),
|
||||
witness,
|
||||
decimals,
|
||||
ctx
|
||||
)
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
public fun take_treasury_cap<CoinType>(
|
||||
setup: WrappedAssetSetup<CoinType, V__CURRENT>
|
||||
): TreasuryCap<CoinType> {
|
||||
let WrappedAssetSetup {
|
||||
id,
|
||||
treasury_cap
|
||||
} = setup;
|
||||
object::delete(id);
|
||||
|
||||
treasury_cap
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::create_wrapped_tests {
|
||||
use sui::coin::{Self};
|
||||
use sui::test_scenario::{Self};
|
||||
use sui::test_utils::{Self};
|
||||
use sui::tx_context::{Self};
|
||||
use wormhole::wormhole_scenario::{parse_and_verify_vaa};
|
||||
|
||||
use token_bridge::asset_meta::{Self};
|
||||
use token_bridge::coin_wrapped_12::{Self};
|
||||
use token_bridge::coin_wrapped_7::{Self};
|
||||
use token_bridge::create_wrapped::{Self};
|
||||
use token_bridge::state::{Self};
|
||||
use token_bridge::string_utils::{Self};
|
||||
use token_bridge::token_bridge_scenario::{
|
||||
register_dummy_emitter,
|
||||
return_state,
|
||||
set_up_wormhole_and_token_bridge,
|
||||
take_state,
|
||||
two_people
|
||||
};
|
||||
use token_bridge::token_registry::{Self};
|
||||
use token_bridge::vaa::{Self};
|
||||
use token_bridge::version_control::{V__0_2_0 as V__CURRENT};
|
||||
use token_bridge::wrapped_asset::{Self};
|
||||
|
||||
struct NOT_A_WITNESS has drop {}
|
||||
|
||||
struct CREATE_WRAPPED_TESTS has drop {}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = create_wrapped::E_BAD_WITNESS)]
|
||||
fun test_cannot_prepare_registration_bad_witness() {
|
||||
let ctx = &mut tx_context::dummy();
|
||||
|
||||
// You shall not pass!
|
||||
let wrapped_asset_setup =
|
||||
create_wrapped::prepare_registration<NOT_A_WITNESS, V__CURRENT>(
|
||||
NOT_A_WITNESS {},
|
||||
3,
|
||||
ctx
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
test_utils::destroy(wrapped_asset_setup);
|
||||
|
||||
abort 42
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = create_wrapped::E_INVALID_COIN_MODULE_NAME)]
|
||||
fun test_cannot_prepare_registration_invalid_coin_module_name() {
|
||||
let ctx = &mut tx_context::dummy();
|
||||
|
||||
// You shall not pass!
|
||||
let wrapped_asset_setup =
|
||||
create_wrapped::prepare_registration<
|
||||
CREATE_WRAPPED_TESTS,
|
||||
V__CURRENT
|
||||
>(
|
||||
CREATE_WRAPPED_TESTS {},
|
||||
3,
|
||||
ctx
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
test_utils::destroy(wrapped_asset_setup);
|
||||
|
||||
abort 42
|
||||
}
|
||||
|
||||
#[test]
|
||||
fun test_complete_and_update_attestation() {
|
||||
let (caller, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(caller);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Set up contracts.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register foreign emitter on chain ID == 2.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Ignore effects. Make sure `coin_deployer` receives
|
||||
// `WrappedAssetSetup`.
|
||||
test_scenario::next_tx(scenario, coin_deployer);
|
||||
|
||||
// Publish coin.
|
||||
let (
|
||||
wrapped_asset_setup,
|
||||
upgrade_cap
|
||||
) =
|
||||
create_wrapped::new_setup_current(
|
||||
CREATE_WRAPPED_TESTS {},
|
||||
8,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
let verified_vaa =
|
||||
parse_and_verify_vaa(scenario, coin_wrapped_12::encoded_vaa());
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
let coin_meta = test_scenario::take_shared(scenario);
|
||||
|
||||
create_wrapped::complete_registration(
|
||||
&mut token_bridge_state,
|
||||
&mut coin_meta,
|
||||
wrapped_asset_setup,
|
||||
upgrade_cap,
|
||||
msg
|
||||
);
|
||||
|
||||
let (
|
||||
token_address,
|
||||
token_chain,
|
||||
native_decimals,
|
||||
symbol,
|
||||
name
|
||||
) = asset_meta::unpack_test_only(coin_wrapped_12::token_meta());
|
||||
|
||||
// Check registry.
|
||||
{
|
||||
let registry = state::borrow_token_registry(&token_bridge_state);
|
||||
let verified =
|
||||
token_registry::verified_asset<CREATE_WRAPPED_TESTS>(registry);
|
||||
assert!(token_registry::is_wrapped(&verified), 0);
|
||||
|
||||
let asset =
|
||||
token_registry::borrow_wrapped<CREATE_WRAPPED_TESTS>(registry);
|
||||
assert!(wrapped_asset::total_supply(asset) == 0, 0);
|
||||
|
||||
// Decimals are capped for this wrapped asset.
|
||||
assert!(coin::get_decimals(&coin_meta) == 8, 0);
|
||||
|
||||
// Check metadata against asset metadata.
|
||||
let info = wrapped_asset::info(asset);
|
||||
assert!(wrapped_asset::token_chain(info) == token_chain, 0);
|
||||
assert!(wrapped_asset::token_address(info) == token_address, 0);
|
||||
assert!(
|
||||
wrapped_asset::native_decimals(info) == native_decimals,
|
||||
0
|
||||
);
|
||||
assert!(coin::get_symbol(&coin_meta) == string_utils::to_ascii(&symbol), 0);
|
||||
assert!(coin::get_name(&coin_meta) == name, 0);
|
||||
};
|
||||
|
||||
|
||||
// Now update metadata.
|
||||
let verified_vaa =
|
||||
parse_and_verify_vaa(
|
||||
scenario,
|
||||
coin_wrapped_12::encoded_updated_vaa()
|
||||
);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
create_wrapped::update_attestation<CREATE_WRAPPED_TESTS>(
|
||||
&mut token_bridge_state,
|
||||
&mut coin_meta,
|
||||
msg
|
||||
);
|
||||
|
||||
// Check updated name and symbol.
|
||||
let (
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
new_symbol,
|
||||
new_name
|
||||
) = asset_meta::unpack_test_only(coin_wrapped_12::updated_token_meta());
|
||||
|
||||
assert!(symbol != new_symbol, 0);
|
||||
|
||||
assert!(coin::get_symbol(&coin_meta) == string_utils::to_ascii(&new_symbol), 0);
|
||||
|
||||
assert!(name != new_name, 0);
|
||||
assert!(coin::get_name(&coin_meta) == new_name, 0);
|
||||
|
||||
test_scenario::return_shared(coin_meta);
|
||||
|
||||
// Clean up.
|
||||
return_state(token_bridge_state);
|
||||
|
||||
// Done.
|
||||
test_scenario::end(my_scenario);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = wrapped_asset::E_ASSET_META_MISMATCH)]
|
||||
fun test_cannot_update_attestation_wrong_canonical_info() {
|
||||
let (caller, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(caller);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Set up contracts.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register foreign emitter on chain ID == 2.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Ignore effects. Make sure `coin_deployer` receives
|
||||
// `WrappedAssetSetup`.
|
||||
test_scenario::next_tx(scenario, coin_deployer);
|
||||
|
||||
// Publish coin.
|
||||
let (
|
||||
wrapped_asset_setup,
|
||||
upgrade_cap
|
||||
) =
|
||||
create_wrapped::new_setup_current(
|
||||
CREATE_WRAPPED_TESTS {},
|
||||
8,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
let verified_vaa =
|
||||
parse_and_verify_vaa(scenario, coin_wrapped_12::encoded_vaa());
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
let coin_meta = test_scenario::take_shared(scenario);
|
||||
|
||||
create_wrapped::complete_registration(
|
||||
&mut token_bridge_state,
|
||||
&mut coin_meta,
|
||||
wrapped_asset_setup,
|
||||
upgrade_cap,
|
||||
msg
|
||||
);
|
||||
// This VAA is for COIN_WRAPPED_7 metadata, which disagrees with
|
||||
// COIN_WRAPPED_12.
|
||||
let invalid_asset_meta_vaa = coin_wrapped_7::encoded_vaa();
|
||||
|
||||
let verified_vaa =
|
||||
parse_and_verify_vaa(scenario, invalid_asset_meta_vaa);
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
// You shall not pass!
|
||||
create_wrapped::update_attestation<CREATE_WRAPPED_TESTS>(
|
||||
&mut token_bridge_state,
|
||||
&mut coin_meta,
|
||||
msg
|
||||
);
|
||||
|
||||
abort 42
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = state::E_VERSION_MISMATCH)]
|
||||
fun test_cannot_complete_registration_version_mismatch() {
|
||||
let (caller, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(caller);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Set up contracts.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register foreign emitter on chain ID == 2.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Ignore effects. Make sure `coin_deployer` receives
|
||||
// `WrappedAssetSetup`.
|
||||
test_scenario::next_tx(scenario, coin_deployer);
|
||||
|
||||
// Publish coin.
|
||||
let (
|
||||
wrapped_asset_setup,
|
||||
upgrade_cap
|
||||
) =
|
||||
create_wrapped::new_setup_test_only(
|
||||
token_bridge::version_control::dummy(),
|
||||
CREATE_WRAPPED_TESTS {},
|
||||
8,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
let verified_vaa =
|
||||
parse_and_verify_vaa(scenario, coin_wrapped_12::encoded_vaa());
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
let coin_meta = test_scenario::take_shared(scenario);
|
||||
|
||||
create_wrapped::complete_registration(
|
||||
&mut token_bridge_state,
|
||||
&mut coin_meta,
|
||||
wrapped_asset_setup,
|
||||
upgrade_cap,
|
||||
msg
|
||||
);
|
||||
|
||||
abort 42
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = wormhole::package_utils::E_NOT_CURRENT_VERSION)]
|
||||
fun test_cannot_complete_registration_outdated_version() {
|
||||
let (caller, coin_deployer) = two_people();
|
||||
let my_scenario = test_scenario::begin(caller);
|
||||
let scenario = &mut my_scenario;
|
||||
|
||||
// Set up contracts.
|
||||
let wormhole_fee = 350;
|
||||
set_up_wormhole_and_token_bridge(scenario, wormhole_fee);
|
||||
|
||||
// Register foreign emitter on chain ID == 2.
|
||||
let expected_source_chain = 2;
|
||||
register_dummy_emitter(scenario, expected_source_chain);
|
||||
|
||||
// Ignore effects. Make sure `coin_deployer` receives
|
||||
// `WrappedAssetSetup`.
|
||||
test_scenario::next_tx(scenario, coin_deployer);
|
||||
|
||||
// Publish coin.
|
||||
let (
|
||||
wrapped_asset_setup,
|
||||
upgrade_cap
|
||||
) =
|
||||
create_wrapped::new_setup_current(
|
||||
CREATE_WRAPPED_TESTS {},
|
||||
8,
|
||||
test_scenario::ctx(scenario)
|
||||
);
|
||||
|
||||
let token_bridge_state = take_state(scenario);
|
||||
|
||||
let verified_vaa =
|
||||
parse_and_verify_vaa(scenario, coin_wrapped_12::encoded_vaa());
|
||||
let msg = vaa::verify_only_once(&mut token_bridge_state, verified_vaa);
|
||||
|
||||
let coin_meta = test_scenario::take_shared(scenario);
|
||||
|
||||
// Conveniently roll version back.
|
||||
state::reverse_migrate_version(&mut token_bridge_state);
|
||||
|
||||
// Simulate executing with an outdated build by upticking the minimum
|
||||
// required version for `publish_message` to something greater than
|
||||
// this build.
|
||||
state::migrate_version_test_only(
|
||||
&mut token_bridge_state,
|
||||
token_bridge::version_control::previous_version_test_only(),
|
||||
token_bridge::version_control::next_version()
|
||||
);
|
||||
|
||||
// You shall not pass!
|
||||
create_wrapped::complete_registration(
|
||||
&mut token_bridge_state,
|
||||
&mut coin_meta,
|
||||
wrapped_asset_setup,
|
||||
upgrade_cap,
|
||||
msg
|
||||
);
|
||||
|
||||
abort 42
|
||||
}
|
||||
}
|
|
@ -0,0 +1,167 @@
|
|||
// SPDX-License-Identifier: Apache 2
|
||||
|
||||
/// This module implements a container that stores the token transfer amount
|
||||
/// encoded in a Token Bridge message. These amounts are capped at 8 decimals.
|
||||
/// This means that any amount of a coin whose metadata defines its decimals
|
||||
/// as some value greater than 8, the encoded amount will be normalized to
|
||||
/// eight decimals (which will lead to some residual amount after the transfer).
|
||||
/// For inbound transfers, this amount will be denormalized (scaled by the same
|
||||
/// decimal difference).
|
||||
module token_bridge::normalized_amount {
|
||||
use sui::math::{Self};
|
||||
use wormhole::bytes32::{Self};
|
||||
use wormhole::cursor::{Cursor};
|
||||
|
||||
/// The amounts in the token bridge payload are truncated to 8 decimals
|
||||
/// in each of the contracts when sending tokens out, so there's no
|
||||
/// precision beyond 10^-8. We could preserve the original number of
|
||||
/// decimals when creating wrapped assets, and "untruncate" the amounts
|
||||
/// on the way out by scaling back appropriately. This is what most
|
||||
/// other chains do, but untruncating from 8 decimals to 18 decimals
|
||||
/// loses log2(10^10) ~ 33 bits of precision, which we cannot afford on
|
||||
/// Aptos (and Solana), as the coin type only has 64bits to begin with.
|
||||
/// Contrast with Ethereum, where amounts are 256 bits.
|
||||
/// So we cap the maximum decimals at 8 when creating a wrapped token.
|
||||
const MAX_DECIMALS: u8 = 8;
|
||||
|
||||
/// Container holding the value decoded from a Token Bridge transfer.
|
||||
struct NormalizedAmount has store, copy, drop {
|
||||
value: u64
|
||||
}
|
||||
|
||||
public fun max_decimals(): u8 {
|
||||
MAX_DECIMALS
|
||||
}
|
||||
|
||||
/// Utility function to cap decimal amount to 8.
|
||||
public fun cap_decimals(decimals: u8): u8 {
|
||||
if (decimals > MAX_DECIMALS) {
|
||||
MAX_DECIMALS
|
||||
} else {
|
||||
decimals
|
||||
}
|
||||
}
|
||||
|
||||
/// Create new `NormalizedAmount` of zero.
|
||||
public fun default(): NormalizedAmount {
|
||||
new(0)
|
||||
}
|
||||
|
||||
/// Retrieve underlying value.
|
||||
public fun value(self: &NormalizedAmount): u64 {
|
||||
self.value
|
||||
}
|
||||
|
||||
/// Retrieve underlying value as `u256`.
|
||||
public fun to_u256(norm: NormalizedAmount): u256 {
|
||||
(take_value(norm) as u256)
|
||||
}
|
||||
|
||||
/// Create new `NormalizedAmount` using raw amount and specified decimals.
|
||||
public fun from_raw(amount: u64, decimals: u8): NormalizedAmount {
|
||||
if (amount == 0) {
|
||||
default()
|
||||
} else if (decimals > MAX_DECIMALS) {
|
||||
new(amount / math::pow(10, decimals - MAX_DECIMALS))
|
||||
} else {
|
||||
new(amount)
|
||||
}
|
||||
}
|
||||
|
||||
/// Denormalize `NormalizedAmount` using specified decimals.
|
||||
public fun to_raw(norm: NormalizedAmount, decimals: u8): u64 {
|
||||
let value = take_value(norm);
|
||||
|
||||
if (value > 0 && decimals > MAX_DECIMALS) {
|
||||
value * math::pow(10, decimals - MAX_DECIMALS)
|
||||
} else {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform `NormalizedAmount` to serialized (big-endian) u256.
|
||||
public fun to_bytes(norm: NormalizedAmount): vector<u8> {
|
||||
bytes32::to_bytes(bytes32::from_u256_be(to_u256(norm)))
|
||||
}
|
||||
|
||||
/// Read 32 bytes from `Cursor` and deserialize to u64, ensuring no
|
||||
/// overflow.
|
||||
public fun take_bytes(cur: &mut Cursor<u8>): NormalizedAmount {
|
||||
// Amounts are encoded with 32 bytes.
|
||||
new(bytes32::to_u64_be(bytes32::take_bytes(cur)))
|
||||
}
|
||||
|
||||
fun new(value: u64): NormalizedAmount {
|
||||
NormalizedAmount {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
fun take_value(norm: NormalizedAmount): u64 {
|
||||
let NormalizedAmount { value } = norm;
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
#[test_only]
|
||||
module token_bridge::normalized_amount_test {
|
||||
use wormhole::bytes::{Self};
|
||||
use wormhole::cursor::{Self};
|
||||
|
||||
use token_bridge::normalized_amount::{Self};
|
||||
|
||||
#[test]
|
||||
fun test_from_and_to_raw() {
|
||||
// Use decimals > 8 to check truncation.
|
||||
let decimals = 9;
|
||||
let raw_amount = 12345678910111;
|
||||
let normalized = normalized_amount::from_raw(raw_amount, decimals);
|
||||
let denormalized = normalized_amount::to_raw(normalized, decimals);
|
||||
assert!(denormalized == 10 * (raw_amount / 10), 0);
|
||||
|
||||
// Use decimals <= 8 to check raw amount recovery.
|
||||
let decimals = 5;
|
||||
let normalized = normalized_amount::from_raw(raw_amount, decimals);
|
||||
let denormalized = normalized_amount::to_raw(normalized, decimals);
|
||||
assert!(denormalized == raw_amount, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fun test_take_bytes() {
|
||||
let cur =
|
||||
cursor::new(
|
||||
x"000000000000000000000000000000000000000000000000ffffffffffffffff"
|
||||
);
|
||||
|
||||
let norm = normalized_amount::take_bytes(&mut cur);
|
||||
assert!(
|
||||
normalized_amount::value(&norm) == ((1u256 << 64) - 1 as u64),
|
||||
0
|
||||
);
|
||||
|
||||
// Clean up.
|
||||
cursor::destroy_empty(cur);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[expected_failure(abort_code = wormhole::bytes32::E_U64_OVERFLOW)]
|
||||
fun test_cannot_take_bytes_overflow() {
|
||||
let encoded_overflow =
|
||||
x"0000000000000000000000000000000000000000000000010000000000000000";
|
||||
|
||||
let amount = {
|
||||
let cur = cursor::new(encoded_overflow);
|
||||
let value = bytes::take_u256_be(&mut cur);
|
||||
cursor::destroy_empty(cur);
|
||||
value
|
||||
};
|
||||
assert!(amount == (1 << 64), 0);
|
||||
|
||||
let cur = cursor::new(encoded_overflow);
|
||||
|
||||
// You shall not pass!
|
||||
normalized_amount::take_bytes(&mut cur);
|
||||
|
||||
abort 42
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
/// This module recovers a Diem-style storage model where objects are collected
|
||||
/// into a heterogeneous global storage, identified by their type.
|
||||
///
|
||||
/// Under the hood, it uses dynamic object fields, but set up in a way that the
|
||||
/// key is derived from the value's type.
|
||||
module token_bridge::dynamic_set {
|
||||
use sui::dynamic_object_field as ofield;
|
||||
use sui::object::{UID};
|
||||
|
||||
/// Wrap the value type. Avoids key collisions with other uses of dynamic
|
||||
/// fields.
|
||||
struct Wrapper<phantom Value> has copy, drop, store {
|
||||
}
|
||||
|
||||
public fun add<Value: key + store>(
|
||||
object: &mut UID,
|
||||
value: Value,
|
||||
) {
|
||||
ofield::add(object, Wrapper<Value>{}, value)
|
||||
}
|
||||
|
||||
public fun borrow<Value: key + store>(
|
||||
object: &UID,
|
||||
): &Value {
|
||||
ofield::borrow(object, Wrapper<Value>{})
|
||||
}
|
||||
|
||||
public fun borrow_mut<Value: key + store>(
|
||||
object: &mut UID,
|
||||
): &mut Value {
|
||||
ofield::borrow_mut(object, Wrapper<Value>{})
|
||||
}
|
||||
|
||||
public fun remove<Value: key + store>(
|
||||
object: &mut UID,
|
||||
): Value {
|
||||
ofield::remove(object, Wrapper<Value>{})
|
||||
}
|
||||
|
||||
public fun exists_<Value: key + store>(
|
||||
object: &UID,
|
||||
): bool {
|
||||
ofield::exists_<Wrapper<Value>>(object, Wrapper<Value>{})
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue