sui: redesign Wormhole and Token Bridge contracts

This commit is contained in:
A5 Pickle 2023-05-02 16:22:30 +00:00 committed by A5 Pickle
parent 67c7c86419
commit 760db3c810
180 changed files with 26627 additions and 6973 deletions

View File

@ -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

2
.gitignore vendored
View File

@ -31,3 +31,5 @@ bigtable-writer.json
/solana/artifacts-mainnet/
/ethereum/out/
/ethereum/cache/
sui.log.*
sui/examples/wrapped_coin

View File

@ -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

2
sui/.gitignore vendored
View File

@ -1,2 +1,2 @@
env.sh
deploy.out
sui.log.*

11
sui/Docker.md Normal file → Executable file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ..

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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)
}
}

12
sui/devnet/client.yaml Normal file
View File

@ -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"

45
sui/devnet/fullnode.yaml Normal file
View File

@ -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

BIN
sui/devnet/genesis.blob Normal file

Binary file not shown.

297
sui/devnet/network.yaml Normal file

File diff suppressed because one or more lines are too long

7
sui/devnet/sui.keystore Normal file
View File

@ -0,0 +1,7 @@
[
"AB522qKKEsXMTFRD2SG3Het/02S/ZBOugmcH3R1CDG6l",
"AOmPq9B16F3W3ijO/4s9hI6v8LdiYCawKAW31PKpg4Qp",
"AOLhc0ryVWnD5LmqH3kCHruBpVV+68EWjEGu2eC9gndK",
"AKCo1FyhQ0zUpnoZLmGJJ+8LttTrt56W87Ho4vBF+R+8",
"AGA20wtGcwbcNAG4nwapbQ5wIuXwkYQEWFUoSVAxctHb"
]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

1
sui/examples/coins/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build

View File

@ -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

View File

@ -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 = "_"

View File

@ -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" },
]

View File

@ -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"

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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 = "_"

View File

@ -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" },
]

View File

@ -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"

View File

@ -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);
}
}

View File

@ -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.

View File

@ -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"

View File

@ -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)
);
}
}

View File

@ -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"

View File

@ -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!"

View File

@ -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

View File

@ -1,2 +0,0 @@
#
curl -X POST -d '{"FixedAmountRequest":{"recipient": "'"$1"'"}}' -H 'Content-Type: application/json' http://127.0.0.1:5003/gas

View File

@ -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

View File

@ -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)

View File

@ -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\"

View File

@ -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

View File

@ -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\"

View File

@ -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]

View File

@ -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

View File

@ -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": {}
}
}
}

View File

@ -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"
}
}

View File

@ -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]

22
sui/scripts/register_devnet.sh Executable file
View File

@ -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."

View File

@ -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

View File

9
sui/scripts/wait_for_devnet.sh Executable file
View File

@ -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

4
sui/testing/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
node_modules
sui.log.*
./token_bridge/
./wormhole/

13
sui/testing/Makefile Normal file
View File

@ -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

View File

@ -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);
}
});
});
});

View File

@ -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;
}
}
});
});
});

View File

@ -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");
}

View File

@ -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";

View File

@ -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
);
}
}

View File

@ -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}`);
}
}
}

View File

@ -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 });
}

View File

@ -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,
},
});
}

View File

@ -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");
}

View File

@ -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;
}

5917
sui/testing/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
sui/testing/package.json Normal file
View File

@ -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"
}
}

View File

@ -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

View File

@ -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 });
}

View File

@ -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 });
}

View File

@ -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"

View File

@ -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

View File

@ -0,0 +1,7 @@
[
"AB522qKKEsXMTFRD2SG3Het/02S/ZBOugmcH3R1CDG6l",
"AOmPq9B16F3W3ijO/4s9hI6v8LdiYCawKAW31PKpg4Qp",
"AOLhc0ryVWnD5LmqH3kCHruBpVV+68EWjEGu2eC9gndK",
"AKCo1FyhQ0zUpnoZLmGJJ+8LttTrt56W87Ho4vBF+R+8",
"AGA20wtGcwbcNAG4nwapbQ5wIuXwkYQEWFUoSVAxctHb"
]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

12
sui/testing/tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"lib": ["es2020"],
"module": "commonjs",
"target": "es2020",
"strict": true,
"resolveJsonModule": true,
"esModuleInterop": true
}
}

View File

@ -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
)

View File

@ -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=

View File

@ -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())
}
}
}
}

View File

@ -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()

1
sui/token_bridge/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build

View File

@ -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

View File

@ -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 = "_"

View File

@ -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" },
]

View File

@ -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"

View File

@ -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"

View File

@ -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

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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