near: initial commit

This commit is contained in:
Josh Siegel 2022-08-04 15:52:26 +00:00 committed by Evan Gray
parent 536a97066e
commit 4fe9841810
71 changed files with 45245 additions and 3 deletions

View File

@ -45,3 +45,4 @@ COPY --from=const-build /scripts/.env.hex solana/.env
COPY --from=const-build /scripts/.env.hex terra/tools/.env
COPY --from=const-build /scripts/.env.hex cosmwasm/tools/.env
COPY --from=const-build /scripts/.env.hex algorand/.env
COPY --from=const-build /scripts/.env.hex near/.env

View File

@ -38,6 +38,7 @@ config.define_string("bigTableKeyPath", False, "Path to BigTable json key file")
config.define_string("webHost", False, "Public hostname for port forwards")
# Components
config.define_bool("near", False, "Enable Near component")
config.define_bool("algorand", False, "Enable Algorand component")
config.define_bool("evm2", False, "Enable second Eth component")
config.define_bool("solana", False, "Enable Solana component")
@ -60,6 +61,7 @@ gcpProject = cfg.get("gcpProject", "local-dev")
bigTableKeyPath = cfg.get("bigTableKeyPath", "./event_database/devnet_key.json")
webHost = cfg.get("webHost", "localhost")
algorand = cfg.get("algorand", True)
near = cfg.get("near", True)
evm2 = cfg.get("evm2", True)
solana = cfg.get("solana", True)
terra_classic = cfg.get("terra_classic", True)
@ -231,6 +233,14 @@ def build_node_yaml():
"--chainGovernorEnabled"
]
if near:
container["command"] += [
"--nearRPC",
"http://near:3030",
"--nearContract",
"wormhole.test.near"
]
return encode_yaml_stream(node_yaml)
k8s_yaml_with_ns(build_node_yaml())
@ -240,6 +250,8 @@ if evm2:
guardian_resource_deps = guardian_resource_deps + ["eth-devnet2"]
if solana:
guardian_resource_deps = guardian_resource_deps + ["solana-devnet"]
if near:
guardian_resource_deps = guardian_resource_deps + ["near"]
if terra_classic:
guardian_resource_deps = guardian_resource_deps + ["terra-terrad"]
if terra2:
@ -732,3 +744,30 @@ if algorand:
trigger_mode = trigger_mode,
)
if near:
k8s_yaml_with_ns("devnet/near-devnet.yaml")
docker_build(
ref = "near-node",
context = "near",
dockerfile = "near/Dockerfile",
only = ["Dockerfile", "node_builder.sh", "start_node.sh", "README.md", "cert.pem"],
)
docker_build(
ref = "near-contracts",
context = "near",
dockerfile = "near/Dockerfile.contracts",
)
k8s_resource(
"near",
port_forwards = [
port_forward(3030, name = "Node [:3030]", host = webHost),
port_forward(3031, name = "webserver [:3031]", host = webHost),
],
resource_deps = ["const-gen"],
labels = ["near"],
trigger_mode = trigger_mode,
)

View File

@ -656,7 +656,7 @@ function parseAddress(chain: ChainName, address: string): string {
// TODO: is there a better native format for algorand?
return "0x" + evm_address(address);
} else if (chain === "near") {
return "0x" + evm_address(address);
return "0x" + hex(address).substring(2).padStart(64, "0")
} else if (chain === "injective") {
throw Error("INJECTIVE is not supported yet");
} else if (chain === "osmosis") {

62
devnet/near-devnet.yaml Normal file
View File

@ -0,0 +1,62 @@
apiVersion: v1
kind: Service
metadata:
labels:
app: near
name: near
spec:
ports:
- name: node
port: 3030
targetPort: node
selector:
app: near
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
labels:
app: near
name: near
spec:
replicas: 1
selector:
matchLabels:
app: near
serviceName: near
template:
metadata:
labels:
app: near
spec:
containers:
- name: near-node
image: near-node
command:
- /bin/sh
- -c
- /tmp/start_node.sh
ports:
- containerPort: 3030
name: node
protocol: TCP
- containerPort: 3031
name: webserver
protocol: TCP
readinessProbe:
tcpSocket:
port: 3030
- name: near-contracts
image: near-contracts
command:
- /bin/sh
- -c
- "sh devnet_deploy.sh && touch success && sleep infinity"
readinessProbe:
tcpSocket:
port: 3030
periodSeconds: 1
initialDelaySeconds: 15
periodSeconds: 5
restartPolicy: Always

View File

@ -115,6 +115,10 @@ spec:
# - http://algorand:4001
# - --algorandAlgodToken
# - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
# - --nearRPC
# - http://near:3030
# - --nearContract
# - wormhole.test.near
- --solanaContract
- Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o
# - --solanaWS

1
near/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
_sandbox

23
near/Dockerfile Normal file
View File

@ -0,0 +1,23 @@
FROM rust:1.60@sha256:48d3b5baf199dc7c378e775c47b0c40aaf7d8b23eaf67e15b095bbdaaecd1f10 as near-node
# Support additional root CAs
COPY README.md cert.pem* /certs/
# Debian
RUN if [ -e /certs/cert.pem ]; then cp /certs/cert.pem /etc/ssl/certs/ca-certificates.crt; fi
# git
RUN if [ -e /certs/cert.pem ]; then git config --global http.sslCAInfo /certs/cert.pem; fi
RUN rustup update
RUN apt-get install python3 --no-install-recommends
COPY node_builder.sh /tmp
WORKDIR /tmp
RUN ./node_builder.sh
COPY start_node.sh /tmp
RUN rm -rf /tmp/_sandbox
RUN mkdir -p /tmp/sandbox
RUN nearcore/target/release/near-sandbox --home /tmp/_sandbox init

30
near/Dockerfile.contracts Normal file
View File

@ -0,0 +1,30 @@
FROM debian@sha256:2ce44bbc00a79113c296d9d25524e15d423b23303fdbbe20190d2f96e0aeb251 as near-contracts
# Support additional root CAs
COPY README.md cert.pem* /certs/
# Debian
RUN if [ -e /certs/cert.pem ]; then cp /certs/cert.pem /etc/ssl/certs/ca-certificates.crt; fi
# git
RUN if [ -e /certs/cert.pem ]; then git config --global http.sslCAInfo /certs/cert.pem; fi
RUN apt-get update && apt-get install apt-utils && apt-get install -y python3 npm curl --no-install-recommends
ADD setup-rust.sh .
RUN ./setup-rust.sh
RUN mkdir -p /.npm /home/node/appa /home/node/.npm
WORKDIR /home/node/app
RUN chown -R 1000:1000 /home/node
RUN chown -R 1000:1000 /.npm
USER 1000
ADD --chown=1000:1000 package.json .
ADD --chown=1000:1000 package-lock.json .
ADD --chown=1000:1000 .env .env
RUN npm ci
ADD --chown=1000:1000 devnet_deploy.* .
ADD --chown=1000:1000 ./contracts/*/target/wasm32-unknown-unknown/release/*.wasm .

92
near/Makefile Normal file
View File

@ -0,0 +1,92 @@
.PHONY: test
test: build node_modules
npx ts-node test/test.ts
all: node_modules build nearcore
build: contracts/ft/target/wasm32-unknown-unknown/release/near_ft.wasm \
contracts/mock-bridge-integration/target/wasm32-unknown-unknown/release/near_mock_bridge_integration.wasm \
contracts/mock-bridge-token/target/wasm32-unknown-unknown/release/near_mock_bridge_token.wasm \
contracts/nft-bridge/target/wasm32-unknown-unknown/release/near_nft_bridge.wasm \
contracts/nft-wrapped/target/wasm32-unknown-unknown/release/near_nft.wasm \
contracts/token-bridge/target/wasm32-unknown-unknown/release/near_token_bridge.wasm \
contracts/wormhole/target/wasm32-unknown-unknown/release/near_wormhole.wasm
contracts/ft/target/wasm32-unknown-unknown/release/near_ft.wasm: contracts/ft/src/*.rs contracts/ft/Cargo.toml
cd contracts/ft; cargo build --target wasm32-unknown-unknown --release
contracts/mock-bridge-integration/target/wasm32-unknown-unknown/release/near_mock_bridge_integration.wasm: \
contracts/mock-bridge-integration/src/*.rs \
contracts/mock-bridge-integration/Cargo.toml \
contracts/mock-bridge-token/target/wasm32-unknown-unknown/release/near_mock_bridge_token.wasm \
contracts/nft-wrapped/target/wasm32-unknown-unknown/release/near_nft.wasm
cd contracts/mock-bridge-integration; cargo build --target wasm32-unknown-unknown --release
contracts/mock-bridge-token/target/wasm32-unknown-unknown/release/near_mock_bridge_token.wasm : contracts/mock-bridge-token/src/*.rs contracts/mock-bridge-token/Cargo.toml
cd contracts/mock-bridge-token; cargo build --target wasm32-unknown-unknown --release
contracts/nft-bridge/target/wasm32-unknown-unknown/release/near_nft_bridge.wasm: contracts/nft-bridge/src/*.rs contracts/nft-bridge/Cargo.toml contracts/nft-wrapped/target/wasm32-unknown-unknown/release/near_nft.wasm
cd contracts/nft-bridge; cargo build --target wasm32-unknown-unknown --release
contracts/nft-wrapped/target/wasm32-unknown-unknown/release/near_nft.wasm: contracts/nft-wrapped/src/*.rs contracts/nft-wrapped/Cargo.toml
cd contracts/nft-wrapped; cargo build --target wasm32-unknown-unknown --release
contracts/token-bridge/target/wasm32-unknown-unknown/release/near_token_bridge.wasm: contracts/token-bridge/src/*.rs contracts/token-bridge/Cargo.toml contracts/ft/target/wasm32-unknown-unknown/release/near_ft.wasm
cd contracts/token-bridge; cargo build --target wasm32-unknown-unknown --release
contracts/wormhole/target/wasm32-unknown-unknown/release/near_wormhole.wasm: contracts/wormhole/src/*.rs contracts/wormhole/Cargo.toml
cd contracts/wormhole; cargo build --target wasm32-unknown-unknown --release
package-lock.json: package.json
npm install
node_modules: package-lock.json
touch -m node_modules
npm ci
nearcore:
mkdir $@ && \
cd $@ && \
git init && \
git remote add origin https://github.com/near/nearcore && \
git fetch --depth 1 origin c6eb78ab11d0fb7c7bb9dfa6d712aba449a0140b && \
git checkout FETCH_HEAD
cd $@ && make sandbox-release
run: nearcore
-killall -q Python
./start_node.sh
.PHONY: clean
clean:
rm -rf nearcore node_modules contracts/*/target
.PHONY: reset-force
reset-force: build
-echo cleaning and restarting state
-kubectl delete pod guardian-0 --force &
-kubectl delete pod algorand-0 --force &
-kubectl delete pod near-0 --force &
-echo done
.PHONY: reset
reset: build
-echo cleaning and restarting state
-kubectl delete pod guardian-0 &
-kubectl delete pod algorand-0 &
-kubectl delete pod near-0 &
-echo done
.PHONY: cycle
cycle: reset
(cd ../sdk/js; npm run build)
npm ci
ts-node test/sdk.ts
.PHONY: testnet
testnet: build
cp contracts/*/target/wasm32-unknown-unknown/release/*.wasm .
ts-node devnet_deploy.ts

155
near/NOTES.md Normal file
View File

@ -0,0 +1,155 @@
01000000000100bc708675768ba853af2e01c6217317fbb68500a508a39fece15dac27d36c7c804cf94ad9e4b2134209bbdad7c9479d9929c60bda4612a74c61c06efebacc4da401000003cb5d690100000200000000000000000000000026b4afb60d6c903165150c6f0aa14f8016be4aec00000000000000010f01000000000000000000000000d1a269d9b0dfb66cfdaf89cf0c6e6f8df0615ad00002415045f09f9092000000000000000000000000000000000000000000000000004e6f7420616e2041504520f09f90920000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a5268747470733a2f2f636c6f7564666c6172652d697066732e636f6d2f697066732f516d65536a53696e4870506e6d586d73704d6a776958794e367a533445397a63636172694752336a7863615774712f31301212121212121212121212121212121212121212121212121212121212121212000f
--
1) vaa is received for a user for a token where the user has not paid the storage deposit in that token
2) vaa gets submitted without using the SDK which detects this and correctly registers/pays-storage-deposit
choice 1:
VAA gets consumed but the tokens don't get transfered
choice 2:
The token bridge proactively throws money at the token, which it hopes will get refunded,
to pay for storage if it is not already there
VAA gets consumed but the money might not get refunded... due to token implementor being an asshat
// demonstrates how to query the state without setting
// up an account. (View methods only)
const { providers } = require("near-api-js");
//network config (replace testnet with mainnet or betanet)
const provider = new providers.JsonRpcProvider("https://rpc.testnet.near.org");
getState();
async function getState() {
const rawResult = await provider.query({
request_type: "call_function",
account_id: "guest-book.testnet",
method_name: "getMessages",
args_base64: "e30=",
finality: "optimistic",
});
// format result
const res = JSON.parse(Buffer.from(rawResult.result).toString());
console.log(res);
}
import { Account as nearAccount } from "near-api-js";
My impression is:
https://docs.near.org/docs/tutorials/near-indexer
https://thewiki.near.page/events-api
==
kubectl exec -it near-0 -c near-node -- /bin/bash
My NEAR notes so far...
If needed, install `Rust`:
curl https://sh.rustup.rs -sSf | sh
You need at least version 1.56 or later
rustup default 1.56
rustup update
rustup target add wasm32-unknown-unknown
If needed, install `near-cli`:
npm install near-cli -g
To install the npm dependencies of this test program
npm install
for the near sdk, we are dependent on 4.0.0 or later (where the ecrecover API is)
https://docs.rs/near-sdk/4.0.0/near_sdk/index.html
near-sdk = { version = "4.0.0", features = ["unstable"] }
This has been stuck into Cargo.toml
to bring up the sandbox, start a tmux window and run
rm -rf _sandbox
mkdir -p _sandbox
near-sandbox --home _sandbox init
near-sandbox --home _sandbox run
https://docs.near.org/docs/develop/contracts/sandbox
First thing, lets put this in a docker in Tilt..
vaa_verify?
near-sdk-rs/near-sdk/src/environment/env.rs: (still unstable)
/// Recovers an ECDSA signer address from a 32-byte message `hash` and a corresponding `signature`
/// along with `v` recovery byte.
///
/// Takes in an additional flag to check for malleability of the signature
/// which is generally only ideal for transactions.
///
/// Returns 64 bytes representing the public key if the recovery was successful.
#[cfg(feature = "unstable")]
pub fn ecrecover(
hash: &[u8],
signature: &[u8],
v: u8,
malleability_flag: bool,
) -> Option<[u8; 64]> {
unsafe {
let return_code = sys::ecrecover(
hash.len() as _,
hash.as_ptr() as _,
signature.len() as _,
signature.as_ptr() as _,
v as u64,
malleability_flag as u64,
ATOMIC_OP_REGISTER,
);
if return_code == 0 {
None
} else {
Some(read_register_fixed_64(ATOMIC_OP_REGISTER))
}
}
}
you can look for test_ecrecover() in the same file...
When building the sandbox, it is on port 3030 and we will need access to the validator_key.json...
curl http://localhost:3031/validator_key.json
function getConfig(env) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
contractAccount: "wormhole.test.near",
keyPath: "./_sandbox/validator_key.json",
};
}
}
.function_call(
b"new".to_vec(),
ft,
data.to_vec(),
vaa.sequence,
)

8
near/README.md Normal file
View File

@ -0,0 +1,8 @@
-- bridge for nep141/nep148 tokens --
https://nomicon.io/Standards/Tokens/FungibleToken/Core#reference-level-explanation
welcome to wormhole on near
all near tokens and $NEAR itself will go to the token account...

1217
near/contracts/ft/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
[package]
name = "near-ft"
version = "0.1.0"
authors = ["Josh Siegel <jsiegel@jumptrading.com>"]
edition = "2022"
[lib]
crate-type = ["cdylib"]
[dependencies]
near-sdk = { version = "4.0.0" }
near-contract-standards = { version = "4.0.0" }
hex = { version = "0.4.3" }
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "s"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []

View File

@ -0,0 +1,5 @@
../target/wasm32-unknown-unknown/release/near_ft.wasm: *.rs ../Cargo.toml
(cd ..; cargo build --target wasm32-unknown-unknown --release)

View File

@ -0,0 +1,248 @@
use {
near_contract_standards::fungible_token::{
metadata::{
FungibleTokenMetadata,
FungibleTokenMetadataProvider,
},
FungibleToken,
},
near_sdk::{
borsh::{
self,
BorshDeserialize,
BorshSerialize,
},
collections::LazyOption,
env,
json_types::U128,
near_bindgen,
AccountId,
Balance,
PanicOnDefault,
Promise,
PromiseOrValue,
StorageUsage,
},
};
const CHAIN_ID_NEAR: u16 = 15;
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct FTContractMeta {
metadata: FungibleTokenMetadata,
vaa: Vec<u8>,
sequence: u64,
}
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct FTContract {
token: FungibleToken,
meta: LazyOption<FTContractMeta>,
controller: AccountId,
hash: Vec<u8>,
}
pub fn get_string_from_32(v: &[u8]) -> String {
let s = String::from_utf8_lossy(v);
s.chars().filter(|c| c != &'\0').collect()
}
#[near_bindgen]
impl FTContract {
fn on_account_closed(&mut self, account_id: AccountId, balance: Balance) {
env::log_str(&format!("Closed @{} with {}", account_id, balance));
}
fn on_tokens_burned(&mut self, account_id: AccountId, amount: Balance) {
env::log_str(&format!("Account @{} burned {}", account_id, amount));
}
#[init]
pub fn new(metadata: FungibleTokenMetadata, asset_meta: Vec<u8>, seq_number: u64) -> Self {
assert!(!env::state_exists(), "Already initialized");
metadata.assert_valid();
let meta = FTContractMeta {
metadata,
vaa: asset_meta,
sequence: seq_number,
};
let acct = env::current_account_id();
let astr = acct.to_string();
Self {
token: FungibleToken::new(b"ft".to_vec()),
meta: LazyOption::new(b"md".to_vec(), Some(&meta)),
controller: env::predecessor_account_id(),
hash: env::sha256(astr.as_bytes()),
}
}
pub fn update_ft(
&mut self,
metadata: FungibleTokenMetadata,
asset_meta: Vec<u8>,
seq_number: u64,
) {
if env::predecessor_account_id() != self.controller {
env::panic_str("CrossContractInvalidCaller");
}
if seq_number <= self.meta.get().unwrap().sequence {
env::panic_str("AssetMetaDataRollback");
}
let meta = FTContractMeta {
metadata,
vaa: asset_meta,
sequence: seq_number,
};
self.meta.replace(&meta);
}
pub fn vaa_withdraw(
&mut self,
from: AccountId,
amount: u128,
receiver: String,
chain: u16,
fee: u128,
payload: String,
) -> String {
if env::predecessor_account_id() != self.controller {
env::panic_str("CrossContractInvalidCaller");
}
let vaa = self.meta.get().unwrap().vaa;
let mut p = [
// PayloadID uint8 = 1
(if payload.is_empty() { 1 } else { 3 } as u8)
.to_be_bytes()
.to_vec(),
// Amount uint256
vec![0; 24],
(amount as u64).to_be_bytes().to_vec(),
//TokenAddress bytes32
vaa[0..32].to_vec(),
// TokenChain uint16
vaa[32..34].to_vec(),
// To bytes32
vec![0; (64 - receiver.len()) / 2],
hex::decode(receiver).unwrap(),
// ToChain uint16
(chain as u16).to_be_bytes().to_vec(),
]
.concat();
if payload.is_empty() {
p = [p, vec![0; 24], (fee as u64).to_be_bytes().to_vec()].concat();
if p.len() != 133 {
env::panic_str(&format!("paylod1 formatting errro len = {}", p.len()));
}
} else {
p = [p, hex::decode(&payload).unwrap()].concat();
if p.len() != (133 + (payload.len() / 2)) {
env::panic_str(&format!("paylod3 formatting errro len = {}", p.len()));
}
}
self.token.internal_withdraw(&from, amount);
near_contract_standards::fungible_token::events::FtBurn {
owner_id: &from,
amount: &U128::from(amount),
memo: Some("Wormhole burn"),
}
.emit();
hex::encode(p)
}
#[payable]
pub fn vaa_transfer(
&mut self,
amount: u128,
account_id: AccountId,
recipient_chain: u16,
fee: u128,
refund_to: AccountId,
) -> Promise {
if env::predecessor_account_id() != self.controller {
env::panic_str("CrossContractInvalidCaller");
}
if recipient_chain != CHAIN_ID_NEAR {
env::panic_str("InvalidRecipientChain");
}
if amount == 0 {
env::panic_str("ZeroAmountWastesGas");
}
if amount <= fee {
env::panic_str("amount <= fee");
}
let mut deposit: Balance = env::attached_deposit();
if !self.token.accounts.contains_key(&account_id) {
let min_balance = self.storage_balance_bounds().min.0;
if deposit < min_balance {
env::panic_str("The attached deposit is less than the minimum storage balance");
}
self.token.internal_register_account(&account_id);
deposit -= min_balance;
}
self.token.internal_deposit(&account_id, amount - fee);
near_contract_standards::fungible_token::events::FtMint {
owner_id: &account_id,
amount: &U128::from(amount - fee),
memo: Some("wormhole minted tokens"),
}
.emit();
if fee != 0 {
self.token.internal_deposit(&env::signer_account_id(), fee);
near_contract_standards::fungible_token::events::FtMint {
owner_id: &env::signer_account_id(),
amount: &U128::from(fee),
memo: Some("wormhole minted tokens"),
}
.emit();
}
env::log_str("vaa_transfer called in ft");
Promise::new(refund_to).transfer(deposit)
}
pub fn account_storage_usage(&self) -> StorageUsage {
self.token.account_storage_usage
}
/// Return true if the caller is either controller or self
pub fn controller_or_self(&self) -> bool {
let caller = env::predecessor_account_id();
caller == self.controller || caller == env::current_account_id()
}
}
near_contract_standards::impl_fungible_token_core!(FTContract, token, on_tokens_burned);
near_contract_standards::impl_fungible_token_storage!(FTContract, token, on_account_closed);
#[near_bindgen]
impl FungibleTokenMetadataProvider for FTContract {
fn ft_metadata(&self) -> FungibleTokenMetadata {
self.meta.get().unwrap().metadata
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,29 @@
[package]
name = "near-mock-bridge-integration"
version = "0.1.0"
authors = ["Josh Siegel <jsiegel@jumptrading.com>"]
edition = "2022"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "*", features = ["derive"] }
serde_json = "*"
near-sdk = { version = "4.0.0", features = ["unstable"] }
near-contract-standards = { version = "4.0.0" }
hex = { version = "0.4.3" }
near-sys = { version = "0.2.0" }
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "z"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []

View File

@ -0,0 +1,5 @@
../target/wasm32-unknown-unknown/release/near_mock_bridge_integration.wasm: *.rs ../Cargo.toml
(cd ..; cargo build --target wasm32-unknown-unknown --release)

View File

@ -0,0 +1,302 @@
#![allow(unused_variables)]
#![allow(unused_imports)]
use {
near_contract_standards::non_fungible_token::{
metadata::{
NFTContractMetadata,
TokenMetadata,
NFT_METADATA_SPEC,
},
Token,
TokenId,
},
near_sdk::{
borsh::{
self,
BorshDeserialize,
BorshSerialize,
},
env,
ext_contract,
json_types::{
Base64VecU8,
U128,
},
near_bindgen,
utils::is_promise_success,
AccountId,
Balance,
Promise,
PromiseOrValue,
},
};
const BRIDGE_TOKEN_BINARY: &[u8] = include_bytes!(
"../../mock-bridge-token/target/wasm32-unknown-unknown/release/near_mock_bridge_token.wasm"
);
const BRIDGE_NFT_BINARY: &[u8] =
include_bytes!("../../nft-wrapped/target/wasm32-unknown-unknown/release/near_nft.wasm");
/// Initial balance for the BridgeToken contract to cover storage and related.
const BRIDGE_TOKEN_INIT_BALANCE: Balance = 5_860_000_000_000_000_000_000;
#[ext_contract(ext_worm_hole)]
pub trait Wormhole {
fn verify_vaa(&self, vaa: String) -> u32;
fn publish_message(&self, data: String, nonce: u32) -> u64;
}
#[ext_contract(ext_ft_contract)]
pub trait MockFtContract {
fn new() -> Self;
fn airdrop(&self, a: AccountId, amount: u128);
}
#[ext_contract(ext_wormhole)]
pub trait MockWormhole {
fn pass(&self) -> bool;
}
#[ext_contract(ext_nft_contract)]
pub trait MockNftContract {
fn new(owner_id: AccountId, metadata: NFTContractMetadata, seq_number: u64) -> Self;
fn nft_mint(
&mut self,
token_id: TokenId,
token_owner_id: AccountId,
token_metadata: TokenMetadata,
refund_to: AccountId,
) -> Token;
}
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct TokenBridgeTest {
cnt: u32,
ft_name: String,
}
impl Default for TokenBridgeTest {
fn default() -> Self {
Self {
cnt: 0,
ft_name: "".to_string(),
}
}
}
#[near_bindgen]
impl TokenBridgeTest {
#[payable]
pub fn deploy_ft(&mut self, account: String) -> Promise {
let a = AccountId::try_from(account).unwrap();
if self.ft_name == "" {
let name = format!("b{}", env::block_height());
let bridge_token_account = format!("{}.{}", name, env::current_account_id());
self.ft_name = bridge_token_account.clone();
let bridge_token_account_id: AccountId =
AccountId::new_unchecked(bridge_token_account.clone());
let v = BRIDGE_TOKEN_BINARY.to_vec();
Promise::new(bridge_token_account_id.clone())
.create_account()
.transfer(BRIDGE_TOKEN_INIT_BALANCE + (v.len() as u128 * env::storage_byte_cost()))
.add_full_access_key(env::signer_account_pk())
.deploy_contract(v)
// Lets initialize it with useful stuff
.then(ext_ft_contract::ext(bridge_token_account_id.clone()).new())
.then(
ext_ft_contract::ext(bridge_token_account_id)
.with_attached_deposit(BRIDGE_TOKEN_INIT_BALANCE)
.airdrop(a, BRIDGE_TOKEN_INIT_BALANCE),
)
// And then lets tell us we are done!
.then(Self::ext(env::current_account_id()).finish_deploy(bridge_token_account))
} else {
let bridge_token_account_id: AccountId = AccountId::new_unchecked(self.ft_name.clone());
ext_ft_contract::ext(bridge_token_account_id)
.with_attached_deposit(BRIDGE_TOKEN_INIT_BALANCE)
.airdrop(a, BRIDGE_TOKEN_INIT_BALANCE)
.then(Self::ext(env::current_account_id()).finish_deploy(self.ft_name.clone()))
}
}
#[payable]
pub fn chunker(&mut self, s: String) -> Promise {
self.cnt += 1;
env::log_str(&format!(
"mock-bridge-integration/{}#{}: amount: {} cnt: {}",
file!(),
line!(),
env::attached_deposit(),
self.cnt
));
Self::ext(env::current_account_id())
.with_attached_deposit(env::attached_deposit())
.chunks(s)
.then(
Self::ext(env::current_account_id())
.refunder(env::predecessor_account_id(), env::attached_deposit()),
)
}
#[private]
pub fn refunder(&mut self, refund_to: AccountId, amt: Balance) {
if !is_promise_success() {
env::log_str(&format!(
"mock-bridge-integration/{}#{}: refunding {} to {}",
file!(),
line!(),
amt,
refund_to
));
Promise::new(refund_to).transfer(amt);
}
}
#[payable]
pub fn chunks(&mut self, s: String) -> Promise {
self.cnt += 1;
env::log_str(&format!(
"mock-bridge-integration/{}#{}: amount: {} cnt: {}",
file!(),
line!(),
env::attached_deposit(),
self.cnt
));
ext_wormhole::ext(AccountId::new_unchecked("wormhole.test.near".to_string()))
.with_attached_deposit(env::attached_deposit())
.pass()
.then(Self::ext(env::current_account_id()).thrower(s))
}
pub fn thrower(&mut self, s: String) -> Promise {
self.cnt += 1;
env::log_str(&format!(
"mock-bridge-integration/{}#{}: amount: {} cnt: {}",
file!(),
line!(),
env::attached_deposit(),
self.cnt
));
env::panic_str(&s);
}
#[payable]
pub fn deploy_nft(&mut self, account: String) -> Promise {
let a = AccountId::try_from(account).unwrap();
let bridge_nft_account = format!("b{}.{}", env::block_height(), env::current_account_id());
let bridge_nft_account_id: AccountId = AccountId::new_unchecked(bridge_nft_account.clone());
let v = BRIDGE_NFT_BINARY.to_vec();
let md = NFTContractMetadata {
spec: NFT_METADATA_SPEC.to_string(),
name: "RandomNFT".to_string(),
symbol: "RNFT".to_string(),
icon: None,
base_uri: None,
reference: None,
reference_hash: None,
};
Promise::new(bridge_nft_account_id.clone())
.create_account()
.transfer(env::attached_deposit())
.add_full_access_key(env::signer_account_pk())
.deploy_contract(v)
// Lets initialize it with useful stuff
.then(
ext_nft_contract::ext(bridge_nft_account_id.clone())
.with_unused_gas_weight(3)
.new(env::current_account_id(), md, 0),
)
.then(Self::ext(env::current_account_id()).finish_deploy(bridge_nft_account))
}
#[payable]
pub fn mint_nft(
&mut self,
nft: AccountId,
token_id: String,
media: String,
give_to: AccountId,
) -> Promise {
let md = TokenMetadata {
title: Some("Phil ".to_string() + &token_id),
description: Some("George ".to_string() + &token_id),
media: Some(media.clone()),
media_hash: Some(Base64VecU8::from(env::sha256(media.as_bytes()))),
copies: Some(1u64),
issued_at: None,
expires_at: None,
starts_at: None,
updated_at: None,
extra: None,
reference: None,
reference_hash: None,
};
ext_nft_contract::ext(nft)
.with_attached_deposit(BRIDGE_TOKEN_INIT_BALANCE)
.nft_mint(token_id, give_to, md, env::current_account_id())
}
pub fn ft_on_transfer(
&mut self,
sender_id: AccountId,
amount: U128,
msg: String,
) -> PromiseOrValue<U128> {
env::log_str(&msg);
env::panic_str("ft_on_transfer");
}
#[private]
pub fn finish_deploy(&mut self, ret: String) -> String {
if is_promise_success() {
ret
} else {
env::panic_str("bad deploy");
}
}
pub fn payload3(
amount: u128,
token_address: Vec<u8>,
token_chain: u16,
fee: u128,
vaa: String,
) {
env::log_str(&format!(
"mock-bridge-integration/{}#{}: amount: {} vaa: {}",
file!(),
line!(),
amount,
vaa
));
}
#[payable]
pub fn publish_message(&mut self, core: AccountId, p: String) -> Promise {
ext_worm_hole::ext(core)
.with_attached_deposit(env::attached_deposit())
.publish_message(p, env::block_height() as u32)
}
}

1220
near/contracts/mock-bridge-token/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
[package]
name = "near-mock-bridge-token"
version = "0.1.0"
authors = ["Josh Siegel <jsiegel@jumptrading.com>"]
edition = "2022"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "*", features = ["derive"] }
serde_json = "*"
near-sdk = { version = "4.0.0", features = ["unstable"] }
near-contract-standards = { version = "4.0.0" }
hex = { version = "0.4.3" }
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "z"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []

View File

@ -0,0 +1,5 @@
../target/wasm32-unknown-unknown/release/near_mach_bridge_token.wasm: *.rs ../Cargo.toml
(cd ..; cargo build --target wasm32-unknown-unknown --release)

View File

@ -0,0 +1,74 @@
use near_contract_standards::fungible_token::metadata::{
FungibleTokenMetadata, FungibleTokenMetadataProvider, FT_METADATA_SPEC
};
use near_contract_standards::fungible_token::FungibleToken;
use near_sdk::collections::LazyOption;
use near_sdk::json_types::{U128};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::{
env, near_bindgen, AccountId, PanicOnDefault,
PromiseOrValue, StorageUsage,
};
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct MockFTContract {
token: FungibleToken,
meta: LazyOption<FungibleTokenMetadata>,
controller: AccountId
}
#[near_bindgen]
impl MockFTContract {
#[init]
pub fn new() -> Self {
assert!(!env::state_exists(), "Already initialized");
let name = "MockFT".to_string();
let metadata = FungibleTokenMetadata {
spec: FT_METADATA_SPEC.to_string(),
name: name.clone(),
symbol: name,
icon: Some("".to_string()), // Is there ANY way to supply this?
reference: None,
reference_hash: None,
decimals: 9,
};
Self {
token: FungibleToken::new(b"ft".to_vec()),
meta: LazyOption::new(b"md".to_vec(), Some(&metadata)),
controller: env::predecessor_account_id(),
}
}
#[payable]
pub fn airdrop(&mut self, a: AccountId, amount: u128) {
self.storage_deposit(Some(a.clone()), None);
self.token.internal_deposit(&a, amount);
near_contract_standards::fungible_token::events::FtMint {
owner_id: &a,
amount: &U128::from(amount),
memo: Some("wormhole mock minted tokens"),
}
.emit();
}
pub fn account_storage_usage(&self) -> StorageUsage {
self.token.account_storage_usage
}
}
near_contract_standards::impl_fungible_token_core!(MockFTContract, token);
near_contract_standards::impl_fungible_token_storage!(MockFTContract, token);
#[near_bindgen]
impl FungibleTokenMetadataProvider for MockFTContract {
fn ft_metadata(&self) -> FungibleTokenMetadata {
self.meta.get().unwrap()
}
}

1222
near/contracts/nft-bridge/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
[package]
name = "near-nft-bridge"
version = "0.1.0"
authors = ["Josh Siegel <jsiegel@jumptrading.com>"]
edition = "2022"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "*", features = ["derive"] }
serde_json = "*"
near-sdk = { version = "4.0.0", features = ["unstable"] }
near-contract-standards = { version = "4.0.0" }
hex = { version = "0.4.3" }
near-sys = { version = "0.2.0" }
bs58 = "*"
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "s"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []

View File

@ -0,0 +1,10 @@
../target/wasm32-unknown-unknown/release/near_nft_bridge.wasm: *.rs ../Cargo.toml
(cd ..; cargo build --target wasm32-unknown-unknown --release)
.PHONY: clippy
clippy:
(cd ..; cargo clippy)

View File

@ -0,0 +1 @@
../../wormhole/src/byte_utils.rs

View File

@ -0,0 +1,953 @@
#![allow(unused_mut)]
//#![allow(unused_imports)]
//#![allow(unused_variables)]
//#![allow(dead_code)]
use {
near_contract_standards::non_fungible_token::{
metadata::{
NFTContractMetadata,
TokenMetadata,
NFT_METADATA_SPEC,
},
Token,
TokenId,
},
near_sdk::{
borsh::{
self,
BorshDeserialize,
BorshSerialize,
},
collections::{
LookupMap,
UnorderedSet,
},
env,
ext_contract,
json_types::{
Base64VecU8,
},
near_bindgen,
utils::{
assert_one_yocto,
is_promise_success,
},
AccountId,
Balance,
Gas,
PanicOnDefault,
Promise,
PromiseError,
PromiseOrValue,
PublicKey,
},
std::str,
};
pub mod byte_utils;
pub mod state;
use crate::byte_utils::{
get_string_from_32,
ByteUtils,
};
const CHAIN_ID_NEAR: u16 = 15;
const CHAIN_ID_SOL: u16 = 1;
const BRIDGE_NFT_BINARY: &[u8] =
include_bytes!("../../nft-wrapped/target/wasm32-unknown-unknown/release/near_nft.wasm");
/// Initial balance for the BridgeToken contract to cover storage and related.
const TRANSFER_BUFFER: u128 = 2000;
#[ext_contract(ext_nft_contract)]
pub trait NFTContract {
fn new(owner_id: AccountId, metadata: NFTContractMetadata, seq_number: u64) -> Self;
fn nft_transfer(
&mut self,
receiver_id: AccountId,
token_id: TokenId,
approval_id: Option<u64>,
memo: Option<String>,
);
fn update_ft(&mut self, owner_id: AccountId, metadata: NFTContractMetadata, seq_number: u64);
fn nft_token(&self, token_id: TokenId) -> Option<Token>;
fn nft_mint(
&mut self,
token_id: TokenId,
token_owner_id: AccountId,
token_metadata: TokenMetadata,
refund_to: AccountId,
) -> Token;
fn nft_burn(&mut self, token_id: TokenId, from: AccountId, refund_to: AccountId) -> Promise;
}
#[ext_contract(ext_token_bridge)]
pub trait ExtTokenBridge {
fn finish_deploy(&self, token: String, token_id: String);
}
#[ext_contract(ext_worm_hole)]
pub trait Wormhole {
fn verify_vaa(&self, vaa: String) -> u32;
fn publish_message(&self, data: String, nonce: u32) -> u64;
}
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct TokenData {
meta: String,
address: String,
chain: u16,
}
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct NFTBridgeOld {
booted: bool,
core: AccountId,
dups: UnorderedSet<Vec<u8>>,
owner_pk: PublicKey,
emitter_registration: LookupMap<u16, Vec<u8>>,
last_asset: u32,
upgrade_hash: Vec<u8>,
tokens: LookupMap<AccountId, TokenData>,
key_map: LookupMap<Vec<u8>, AccountId>,
hash_map: LookupMap<Vec<u8>, AccountId>,
token_map: LookupMap<Vec<u8>, String>,
}
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct NFTBridge {
booted: bool,
core: AccountId,
gov_idx: u32,
dups: LookupMap<Vec<u8>, bool>,
owner_pk: PublicKey,
emitter_registration: LookupMap<u16, Vec<u8>>,
last_asset: u32,
upgrade_hash: Vec<u8>,
tokens: LookupMap<AccountId, TokenData>,
key_map: LookupMap<Vec<u8>, AccountId>,
hash_map: LookupMap<Vec<u8>, AccountId>,
token_map: LookupMap<Vec<u8>, String>,
bank: LookupMap<AccountId, Balance>,
}
impl Default for NFTBridge {
fn default() -> Self {
Self {
booted: false,
core: AccountId::new_unchecked("".to_string()),
gov_idx: 0,
dups: LookupMap::new(b"d".to_vec()),
owner_pk: env::signer_account_pk(),
emitter_registration: LookupMap::new(b"c".to_vec()),
last_asset: 0,
upgrade_hash: b"".to_vec(),
tokens: LookupMap::new(b"ta".to_vec()),
key_map: LookupMap::new(b"k".to_vec()),
hash_map: LookupMap::new(b"a".to_vec()),
token_map: LookupMap::new(b"tm".to_vec()),
bank: LookupMap::new(b"b".to_vec()),
}
}
}
fn vaa_register_chain(
storage: &mut NFTBridge,
vaa: &state::ParsedVAA,
mut deposit: Balance,
refund_to: &AccountId,
) -> Balance {
let data: &[u8] = &vaa.payload;
let target_chain = data.get_u16(33);
let chain = data.get_u16(35);
if (target_chain != CHAIN_ID_NEAR) && (target_chain != 0) {
refund_and_panic("InvalidREegisterChainChain", refund_to);
}
if storage.emitter_registration.contains_key(&chain) {
refund_and_panic("DuplicateChainRegistration", refund_to);
}
let storage_used = env::storage_usage();
storage
.emitter_registration
.insert(&chain, &data[37..69].to_vec());
let required_cost = (Balance::from(env::storage_usage()) - Balance::from(storage_used))
* env::storage_byte_cost();
if required_cost > deposit {
refund_and_panic("DepositUnderflowForRegistration", refund_to);
}
deposit -= required_cost;
env::log_str(&format!(
"register chain {} to {}",
chain,
hex::encode(&data[37..69])
));
deposit
}
fn vaa_upgrade_contract(
storage: &mut NFTBridge,
vaa: &state::ParsedVAA,
deposit: Balance,
refund_to: &AccountId,
) -> Balance {
let data: &[u8] = &vaa.payload;
let chain = data.get_u16(33);
if chain != CHAIN_ID_NEAR {
refund_and_panic("InvalidContractUpgradeChain", refund_to);
}
let uh = data.get_bytes32(0);
env::log_str(&format!(
"nft-bridge/{}#{}: vaa_update_contract: {}",
file!(),
line!(),
hex::encode(&uh)
));
storage.upgrade_hash = uh.to_vec(); // Too lazy to do proper accounting here...
deposit
}
fn vaa_governance(
storage: &mut NFTBridge,
vaa: &state::ParsedVAA,
gov_idx: u32,
deposit: Balance,
refund_to: &AccountId,
) -> Balance {
if gov_idx != vaa.guardian_set_index {
refund_and_panic("InvalidGovernanceSet", refund_to);
}
if (CHAIN_ID_SOL != vaa.emitter_chain)
|| (hex::decode("0000000000000000000000000000000000000000000000000000000000000004")
.unwrap()
!= vaa.emitter_address)
{
refund_and_panic("InvalidGovernanceEmitter", refund_to);
}
let data: &[u8] = &vaa.payload;
let action = data.get_u8(32);
match action {
1u8 => vaa_register_chain(storage, vaa, deposit, refund_to),
2u8 => vaa_upgrade_contract(storage, vaa, deposit, refund_to),
_ => refund_and_panic("InvalidGovernanceAction", refund_to),
}
}
fn vaa_transfer(
storage: &mut NFTBridge,
vaa: &state::ParsedVAA,
_action: u8,
mut deposit: Balance,
refund_to: AccountId,
) -> PromiseOrValue<bool> {
let data: &[u8] = &vaa.payload;
let mut offset: usize = 1; // offset into data in bytes
let nft_address = data.get_const_bytes::<32>(offset).to_vec();
offset += 32;
let nft_chain = data.get_u16(offset);
offset += 2;
let symbol = data.get_const_bytes::<32>(offset);
offset += 32;
let name = data.get_const_bytes::<32>(offset);
offset += 32;
let token_id_vec = data.get_const_bytes::<32>(offset).to_vec();
let token_id = hex::encode(token_id_vec.clone());
offset += 32;
let uri_length: usize = data.get_u8(offset).into();
offset += 1;
let uri = data.get_bytes(offset, uri_length).to_vec();
offset += uri_length;
let recipient = data.get_const_bytes::<32>(offset).to_vec();
offset += 32;
let recipient_chain = data.get_u16(offset);
if recipient_chain != CHAIN_ID_NEAR {
refund_and_panic("Not directed at this chain", &refund_to);
}
if !storage.hash_map.contains_key(&recipient) {
refund_and_panic("ReceipientNotRegistered", &refund_to);
}
let recipient_account = storage.hash_map.get(&recipient).unwrap();
env::log_str(&format!(
"nft-bridge/{}#{}: {}",
file!(),
line!(),
hex::encode(recipient)
));
let bridge_token_account;
let mut prom = if nft_chain == CHAIN_ID_NEAR {
if !storage.hash_map.contains_key(&nft_address) {
refund_and_panic("ReceipientNotRegistered", &refund_to);
}
deposit -= 1;
let bridge_token_account_id = storage.hash_map.get(&nft_address).unwrap();
bridge_token_account = bridge_token_account_id.to_string();
ext_nft_contract::ext(bridge_token_account_id)
.with_attached_deposit(1)
.nft_transfer(recipient_account, token_id.clone(), None, None)
} else {
// The land of Wormhole assets
let tkey = nft_key(nft_address.clone(), nft_chain);
let base_uri = String::from_utf8(uri).unwrap();
let reference = hex::encode(&vaa.payload);
let storage_used = env::storage_usage();
let token_key = [tkey.clone(), token_id_vec].concat();
storage.token_map.insert(&token_key, &reference);
let storage_used_now = env::storage_usage();
let delta = (storage_used_now - storage_used) as u128 * env::storage_byte_cost();
if delta > deposit {
env::log_str(&format!(
"nft-bridge/{}#{}: vaa_trnsfer: delta: {} bytes: {} needed: {}",
file!(),
line!(),
delta,
storage_used_now - storage_used,
((deposit - delta) / env::storage_byte_cost())
));
refund_and_panic("PrecheckFailedDepositUnderFlow", &refund_to);
}
deposit -= delta;
let n = get_string_from_32(&name);
let s = get_string_from_32(&symbol);
let md = TokenMetadata {
title: Some(n.clone()),
description: Some(s.clone()),
media: Some(base_uri.clone()),
media_hash: Some(Base64VecU8::from(env::sha256(base_uri.as_bytes()))),
copies: Some(1u64),
issued_at: None,
expires_at: None,
starts_at: None,
updated_at: None,
extra: None,
reference: None,
reference_hash: None,
};
if storage.key_map.contains_key(&tkey) {
let dep = deposit;
deposit = 0;
let bridge_token_account_id = storage.key_map.get(&tkey).unwrap();
bridge_token_account = bridge_token_account_id.to_string();
ext_nft_contract::ext(bridge_token_account_id)
.with_attached_deposit(dep)
.nft_mint(token_id.clone(), recipient_account, md, refund_to.clone())
} else {
let ft = NFTContractMetadata {
spec: NFT_METADATA_SPEC.to_string(),
name: n.clone() + " (wormhole)",
symbol: s.clone(),
icon: None,
base_uri: None,
reference: None,
reference_hash: None,
};
let storage_used = env::storage_usage();
storage.last_asset += 1;
let asset_id = storage.last_asset;
bridge_token_account = format!("{}.{}", asset_id, env::current_account_id());
let bridge_token_account_id: AccountId =
AccountId::new_unchecked(bridge_token_account.clone());
let d = TokenData {
meta: reference,
address: hex::encode(nft_address),
chain: nft_chain,
};
storage.tokens.insert(&bridge_token_account_id, &d);
storage.key_map.insert(&tkey, &bridge_token_account_id);
storage.hash_map.insert(
&env::sha256(bridge_token_account.as_bytes()),
&bridge_token_account_id,
);
let storage_used_now = env::storage_usage();
let delta = (storage_used_now - storage_used) as u128 * env::storage_byte_cost();
let cost = ((TRANSFER_BUFFER * 2) + BRIDGE_NFT_BINARY.len() as u128)
* env::storage_byte_cost();
if cost + delta > deposit {
env::log_str(&format!(
"nft-bridge/{}#{}: vaa_trnsfer: cost: {} delta: {} bytes: {} needed: {}",
file!(),
line!(),
cost,
delta,
storage_used_now - storage_used,
((deposit - (cost + delta)) / env::storage_byte_cost())
));
refund_and_panic("PrecheckFailedDepositUnderFlow", &refund_to);
}
deposit -= cost + delta;
let dep = deposit;
deposit = 0;
Promise::new(bridge_token_account_id.clone())
.create_account()
.transfer(cost)
.add_full_access_key(storage.owner_pk.clone())
.deploy_contract(BRIDGE_NFT_BINARY.to_vec())
// Lets initialize it with useful stuff
.then(ext_nft_contract::ext(bridge_token_account_id.clone()).new(
env::current_account_id(),
ft,
vaa.sequence,
))
.then(
ext_nft_contract::ext(bridge_token_account_id)
.with_attached_deposit(dep)
.nft_mint(token_id.clone(), recipient_account, md, refund_to.clone()),
)
}
};
if deposit > 0 {
env::log_str(&format!(
"nft-bridge/{}#{}: refund {} to {}",
file!(),
line!(),
deposit,
env::predecessor_account_id()
));
prom = prom.then(Promise::new(refund_to).transfer(deposit));
}
PromiseOrValue::Promise(prom.then(
ext_token_bridge::ext(env::current_account_id()).finish_deploy(bridge_token_account, token_id),
))
}
fn refund_and_panic(s: &str, refund_to: &AccountId) -> ! {
if env::attached_deposit() > 0 {
env::log_str(&format!(
"nft-bridge/{}#{}: refund {} to {}",
file!(),
line!(),
env::attached_deposit(),
refund_to
));
Promise::new(refund_to.clone()).transfer(env::attached_deposit());
}
env::panic_str(s);
}
fn nft_key(address: Vec<u8>, chain: u16) -> Vec<u8> {
[address, chain.to_be_bytes().to_vec()].concat()
}
#[near_bindgen]
impl NFTBridge {
pub fn emitter(&self) -> (String, String) {
let acct = env::current_account_id();
let astr = acct.to_string();
(astr.clone(), hex::encode(env::sha256(astr.as_bytes())))
}
pub fn is_wormhole(&self, token: &String) -> bool {
let astr = format!(".{}", env::current_account_id().as_str());
token.ends_with(&astr)
}
pub fn deposit_estimates(&self) -> (String, String) {
// This is a worst case if we have to store a lot of data as well as create a new account
let cost =
((TRANSFER_BUFFER * 5) + BRIDGE_NFT_BINARY.len() as u128) * env::storage_byte_cost();
(env::storage_byte_cost().to_string(), cost.to_string())
}
pub fn get_original_asset(&self, token: String) -> (String, u16) {
let account = AccountId::new_unchecked(token);
if !self.tokens.contains_key(&account) {
env::panic_str("UnknownAssetId");
}
let t = self.tokens.get(&account).unwrap();
(t.address, t.chain)
}
pub fn get_foreign_asset(&self, address: String, chain: u16) -> String {
let p = nft_key(hex::decode(address).unwrap(), chain);
if self.key_map.contains_key(&p) {
return self.key_map.get(&p).unwrap().to_string();
}
"".to_string()
}
#[payable]
pub fn register_account(&mut self, account: String) -> String {
let old_storage_cost = env::storage_usage() as u128 * env::storage_byte_cost() as u128;
let account_hash = env::sha256(account.as_bytes());
let ret = hex::encode(&account_hash);
if self.hash_map.contains_key(&account_hash) {
Promise::new(env::predecessor_account_id()).transfer(env::attached_deposit());
return ret;
}
let a = AccountId::new_unchecked(account);
self.hash_map.insert(&account_hash, &a);
let new_storage_cost = env::storage_usage() as u128 * env::storage_byte_cost() as u128;
if new_storage_cost <= old_storage_cost {
refund_and_panic("ImpossibleStorageCost", &env::predecessor_account_id());
}
if (new_storage_cost - old_storage_cost) > env::attached_deposit() {
refund_and_panic("InvalidStorageDeposit", &env::predecessor_account_id());
}
let refund = env::attached_deposit() - (new_storage_cost - old_storage_cost);
if refund > 0 {
Promise::new(env::predecessor_account_id()).transfer(refund);
}
ret
}
pub fn hash_account(&self, account: String) -> (bool, String) {
// Yes, you could hash it yourself but then you wouldn't know
// if it was already registered...
let account_hash = env::sha256(account.as_bytes());
let ret = hex::encode(&account_hash);
(self.hash_map.contains_key(&account_hash), ret)
}
pub fn hash_lookup(&self, hash: String) -> (bool, String) {
let account_hash = hex::decode(&hash).unwrap();
if self.hash_map.contains_key(&account_hash) {
(true, self.hash_map.get(&account_hash).unwrap().to_string())
} else {
(false, "".to_string())
}
}
pub fn is_transfer_completed(&self, vaa: String) -> (bool, bool) {
let h = hex::decode(vaa).expect("invalidVaa");
let pvaa = state::ParsedVAA::parse(&h);
if self.dups.contains_key(&pvaa.hash) {
(true, self.dups.get(&pvaa.hash).unwrap())
} else {
(false, false)
}
}
#[payable]
pub fn submit_vaa(
&mut self,
vaa: String,
mut refund_to: Option<AccountId>,
) -> PromiseOrValue<bool> {
if refund_to == None {
refund_to = Some(env::predecessor_account_id());
}
if env::prepaid_gas() < Gas(300_000_000_000_000) {
env::panic_str("NotEnoughGas");
}
if env::attached_deposit() < (TRANSFER_BUFFER * env::storage_byte_cost()) {
env::panic_str("StorageDepositUnderflow");
}
let h = hex::decode(&vaa).unwrap();
let pvaa = state::ParsedVAA::parse(&h);
if pvaa.version != 1 {
env::panic_str("invalidVersion");
}
// Check if VAA with this hash was already accepted
if self.dups.contains_key(&pvaa.hash) {
let e = self.dups.get(&pvaa.hash).unwrap();
if e {
env::panic_str("alreadyExecuted");
} else {
self.dups.insert(&pvaa.hash, &true);
self.submit_vaa_work(&pvaa, refund_to.unwrap())
}
} else {
let r = refund_to.unwrap();
PromiseOrValue::Promise(
ext_worm_hole::ext(self.core.clone())
.verify_vaa(vaa.clone())
.then(
Self::ext(env::current_account_id())
.with_unused_gas_weight(10)
.with_attached_deposit(env::attached_deposit())
.verify_vaa_callback(pvaa.hash, r.clone()),
)
.then(
Self::ext(env::current_account_id()).refunder(r, env::attached_deposit()),
),
)
}
}
#[private]
pub fn refunder(&mut self, refund_to: AccountId, amt: Balance) {
if !is_promise_success() {
env::log_str(&format!(
"nft-bridge/{}#{}: refunding {} to {}?",
file!(),
line!(),
amt,
refund_to
));
Promise::new(refund_to).transfer(amt);
}
}
#[private] // So, all of wormhole security rests in this one statement?
#[payable]
pub fn verify_vaa_callback(
&mut self,
hash: Vec<u8>,
refund_to: AccountId,
#[callback_result] gov_idx: Result<u32, PromiseError>,
) -> Promise {
if gov_idx.is_err() {
env::panic_str("vaaVerifyFail");
}
self.gov_idx = gov_idx.unwrap();
// Check if VAA with this hash was already accepted
if self.dups.contains_key(&hash) {
env::panic_str("alreadyExecuted2");
}
let storage_used = env::storage_usage();
let mut deposit = env::attached_deposit();
self.dups.insert(&hash, &false);
let required_cost =
(Balance::from(env::storage_usage() - storage_used)) * env::storage_byte_cost();
if required_cost > deposit {
env::panic_str("DepositUnderflowForHash");
}
deposit -= required_cost;
env::log_str(&format!(
"nft-bridge/{}#{}: refunding {} to {}?",
file!(),
line!(),
deposit,
refund_to
));
Promise::new(refund_to).transfer(deposit)
}
#[private] // So, all of wormhole security rests in this one statement?
#[payable]
fn submit_vaa_work(
&mut self,
pvaa: &state::ParsedVAA,
refund_to: AccountId,
) -> PromiseOrValue<bool> {
env::log_str(&format!(
"nft-bridge/{}#{}: submit_vaa_callback: {} {} used: {} prepaid: {}",
file!(),
line!(),
env::attached_deposit(),
env::predecessor_account_id(),
serde_json::to_string(&env::used_gas()).unwrap(),
serde_json::to_string(&env::prepaid_gas()).unwrap()
));
if pvaa.version != 1 {
env::panic_str("invalidVersion");
}
let data: &[u8] = &pvaa.payload;
let governance = data[0..32]
== hex::decode("00000000000000000000000000000000000000000000004e4654427269646765")
.unwrap();
let action = data.get_u8(0);
let deposit = env::attached_deposit();
if governance {
let bal = vaa_governance(self, pvaa, self.gov_idx, deposit, &refund_to);
if bal > 0 {
env::log_str(&format!(
"nft-bridge/{}#{}: refunding {} to {}",
file!(),
line!(),
bal,
refund_to
));
return PromiseOrValue::Promise(Promise::new(refund_to).transfer(bal));
}
return PromiseOrValue::Value(true);
}
if !self.emitter_registration.contains_key(&pvaa.emitter_chain) {
env::log_str(&format!(
"nft-bridge/{}#{}: Chain Not Registered: {}",
file!(),
line!(),
pvaa.emitter_chain
));
refund_and_panic("ChainNotRegistered", &refund_to);
}
let ce = self.emitter_registration.get(&pvaa.emitter_chain).unwrap();
if ce != pvaa.emitter_address {
refund_and_panic("InvalidRegistration", &refund_to);
}
if action == 1u8 {
vaa_transfer(self, &pvaa, action, deposit, refund_to)
} else {
refund_and_panic("invalidPortAction", &refund_to);
}
}
#[private]
pub fn finish_deploy(&mut self, token: String, token_id: String) -> (String, String) {
if is_promise_success() {
(token, token_id)
} else {
env::panic_str("bad deploy");
}
}
pub fn boot_portal(&mut self, core: String) {
if self.owner_pk != env::signer_account_pk() {
env::panic_str("invalidSigner");
}
if self.booted {
env::panic_str("NoDonut");
}
self.booted = true;
self.core = AccountId::try_from(core).unwrap();
let account_hash = env::sha256(env::current_account_id().to_string().as_bytes());
env::log_str(&format!("nft emitter: {}", hex::encode(account_hash)));
}
#[private]
pub fn update_contract_done(
&mut self,
refund_to: near_sdk::AccountId,
storage_used: u64,
attached_deposit: u128,
) {
let delta = (env::storage_usage() as i128 - storage_used as i128)
* env::storage_byte_cost() as i128;
let refund = attached_deposit as i128 - delta;
if refund > 0 {
env::log_str(&format!(
"nft-bridge/{}#{}: update_contract_done: refund {} to {}",
file!(),
line!(),
refund,
refund_to
));
Promise::new(refund_to).transfer(refund as u128);
}
}
#[private]
fn update_contract_work(&mut self, v: Vec<u8>) -> Promise {
if env::attached_deposit() == 0 {
env::panic_str("attach some cash");
}
let s = env::sha256(&v);
env::log_str(&format!(
"nft-bridge/{}#{}: update_contract: {}",
file!(),
line!(),
hex::encode(&s)
));
if s.to_vec() != self.upgrade_hash {
if env::attached_deposit() > 0 {
env::log_str(&format!(
"nft-bridge/{}#{}: refunding {} to {}",
file!(),
line!(),
env::attached_deposit(),
env::predecessor_account_id()
));
Promise::new(env::predecessor_account_id()).transfer(env::attached_deposit());
}
env::panic_str("invalidUpgradeContract");
}
Promise::new(env::current_account_id())
.deploy_contract(v.to_vec())
.then(Self::ext(env::current_account_id()).update_contract_done(
env::predecessor_account_id(),
env::storage_usage(),
env::attached_deposit(),
))
}
//#[allow(clippy::too_many_arguments)]
#[payable]
pub fn initiate_transfer(
&mut self,
asset: AccountId,
token_id: TokenId,
recipient_chain: u16,
recipient: String,
nonce: u32,
) -> Promise {
assert_one_yocto();
if env::prepaid_gas() < Gas(300_000_000_000_000) {
refund_and_panic("NotEnoughGas", &env::predecessor_account_id());
}
if !self.tokens.contains_key(&asset) {
refund_and_panic("UnknownWormholeAsset", &env::predecessor_account_id());
}
let td = self.tokens.get(&asset).unwrap();
let token_key = [
hex::decode(td.address).unwrap(),
td.chain.to_be_bytes().to_vec(),
hex::decode(&token_id).unwrap(),
]
.concat();
if !self.token_map.contains_key(&token_key) {
refund_and_panic("CannotFindMetaDataForToken", &env::predecessor_account_id());
}
let meta = self.token_map.get(&token_key).unwrap();
let astr = format!(".{}", env::current_account_id().as_str());
if asset.to_string().ends_with(&astr) {
ext_nft_contract::ext(asset.clone())
.nft_burn(
token_id.clone(),
env::predecessor_account_id(),
env::predecessor_account_id(),
)
.then(
Self::ext(env::current_account_id())
.with_unused_gas_weight(10)
.initiate_transfer_wormhole(
asset,
token_id,
recipient_chain,
recipient,
nonce,
meta,
env::predecessor_account_id(),
),
)
} else {
refund_and_panic("NativeNFTsUseDifferentAPI", &env::predecessor_account_id());
}
}
#[private]
pub fn initiate_transfer_wormhole(
&mut self,
_asset: AccountId,
_token_id: TokenId,
recipient_chain: u16,
recipient: String,
_nonce: u32,
meta: String,
_caller: AccountId,
) -> Promise {
if !is_promise_success() {
env::panic_str("Failed to burn NFT");
}
let old = hex::decode(meta).unwrap();
let p = [
old[0..(old.len() - 34)].to_vec(),
vec![0; (64 - recipient.len()) / 2],
hex::decode(recipient).unwrap(),
(recipient_chain as u16).to_be_bytes().to_vec(),
]
.concat();
if old.len() != p.len() {
refund_and_panic("formatting error", &env::predecessor_account_id());
}
ext_worm_hole::ext(self.core.clone())
.publish_message(hex::encode(p), env::block_height() as u32)
}
}
// let result = await userAccount.functionCall({
// contractId: config.tokenAccount,
// methodName: "update_contract",
// args: wormholeContract,
// attachedDeposit: "12500000000000000000000",
// gas: 300000000000000,
// });
#[no_mangle]
pub extern "C" fn update_contract() {
env::setup_panic_hook();
let mut contract: NFTBridge = env::state_read().expect("Contract is not initialized");
contract.update_contract_work(env::input().unwrap());
}

View File

@ -0,0 +1 @@
../../wormhole/src/state.rs

1217
near/contracts/nft-wrapped/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
[package]
name = "near-nft"
version = "0.1.0"
authors = ["Josh Siegel <jsiegel@jumptrading.com>"]
edition = "2022"
[lib]
crate-type = ["cdylib"]
[dependencies]
near-sdk = { version = "4.0.0" }
near-contract-standards = { version = "4.0.0" }
hex = { version = "0.4.3" }
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "s"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []

View File

@ -0,0 +1,7 @@
../target/wasm32-unknown-unknown/release/near_ft.wasm: *.rs ../Cargo.toml
(cd ..; cargo build --target wasm32-unknown-unknown --release)
.PHONY: clippy
clippy:
(cd ..; cargo clippy)

View File

@ -0,0 +1,158 @@
use near_contract_standards::non_fungible_token::metadata::{
NFTContractMetadata, NonFungibleTokenMetadataProvider, TokenMetadata,
};
use near_contract_standards::non_fungible_token::NonFungibleToken;
use near_contract_standards::non_fungible_token::{Token, TokenId};
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::collections::LazyOption;
use near_sdk::{
env, near_bindgen, require, AccountId, BorshStorageKey, PanicOnDefault, Promise, PromiseOrValue,
};
use near_contract_standards::non_fungible_token::events::{NftBurn, NftMint};
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
tokens: NonFungibleToken,
metadata: LazyOption<NFTContractMetadata>,
seq_number: u64,
}
#[derive(BorshSerialize, BorshStorageKey)]
enum StorageKey {
NonFungibleToken,
Metadata,
TokenMetadata,
Enumeration,
Approval,
}
#[near_bindgen]
impl Contract {
#[init]
pub fn new(owner_id: AccountId, metadata: NFTContractMetadata, seq_number: u64) -> Self {
require!(!env::state_exists(), "Already initialized");
metadata.assert_valid();
Self {
tokens: NonFungibleToken::new(
StorageKey::NonFungibleToken,
owner_id,
Some(StorageKey::TokenMetadata),
Some(StorageKey::Enumeration),
Some(StorageKey::Approval),
),
metadata: LazyOption::new(StorageKey::Metadata, Some(&metadata)),
seq_number,
}
}
#[payable]
pub fn nft_mint(
&mut self,
token_id: TokenId,
token_owner_id: AccountId,
token_metadata: TokenMetadata,
refund_to: AccountId,
) -> Token {
assert_eq!(
env::predecessor_account_id(),
self.tokens.owner_id,
"Unauthorized"
);
let token = self.tokens.internal_mint_with_refund(
token_id,
token_owner_id,
Some(token_metadata),
Some(refund_to),
);
NftMint {
owner_id: &token.owner_id,
token_ids: &[&token.token_id],
memo: Some("wormhole nft"),
}
.emit();
token
}
pub fn nft_burn(
&mut self,
token_id: TokenId,
from: AccountId,
refund_to: AccountId,
) -> Promise {
assert_eq!(
env::predecessor_account_id(),
self.tokens.owner_id,
"Unauthorized"
);
let owner = self
.tokens
.owner_by_id
.get(&token_id)
.expect("unknown token id");
if owner != from {
env::panic_str("owner is not who we expected it was")
}
let storage_used = env::storage_usage();
// A lot of moving parts here.. code reviewers.. did I get it
// all? Hard to believe nobody has implemented burn in the
// standard SDK. Googling around found me some other NFT
// contracts that tried to implement it but they didn't get
// the storage management correct.
if let Some(tokens_per_owner) = &mut self.tokens.tokens_per_owner {
// owner_tokens should always exist, so call `unwrap` without guard
let mut owner_tokens = tokens_per_owner.get(&from).unwrap_or_else(|| {
env::panic_str("Unable to access tokens per owner in unguarded call.")
});
owner_tokens.remove(&token_id);
if owner_tokens.is_empty() {
tokens_per_owner.remove(&from);
} else {
tokens_per_owner.insert(&from, &owner_tokens);
}
}
self.tokens.owner_by_id.remove(&token_id);
if let Some(next_approval_id_by_id) = &mut self.tokens.next_approval_id_by_id {
next_approval_id_by_id.remove(&token_id);
}
if let Some(approvals_by_id) = &mut self.tokens.approvals_by_id {
approvals_by_id.remove(&token_id);
}
if let Some(token_metadata_by_id) = &mut self.tokens.token_metadata_by_id {
token_metadata_by_id.remove(&token_id);
}
NftBurn {
owner_id: &from,
token_ids: &[&token_id],
authorized_id: Some(&env::predecessor_account_id()),
memo: Some("wormhole nft"),
}
.emit();
let storage_freed = storage_used - env::storage_usage();
Promise::new(refund_to).transfer(storage_freed as u128 * env::storage_byte_cost())
}
}
near_contract_standards::impl_non_fungible_token_core!(Contract, tokens);
near_contract_standards::impl_non_fungible_token_approval!(Contract, tokens);
near_contract_standards::impl_non_fungible_token_enumeration!(Contract, tokens);
#[near_bindgen]
impl NonFungibleTokenMetadataProvider for Contract {
fn nft_metadata(&self) -> NFTContractMetadata {
self.metadata.get().unwrap()
}
}

1222
near/contracts/token-bridge/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,30 @@
[package]
name = "near-token-bridge"
version = "0.1.0"
authors = ["Josh Siegel <jsiegel@jumptrading.com>"]
edition = "2022"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "*", features = ["derive"] }
serde_json = "*"
near-sdk = { version = "4.0.0", features = ["unstable"] }
near-contract-standards = { version = "4.0.0" }
hex = { version = "0.4.3" }
near-sys = { version = "0.2.0" }
bs58 = "*"
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "s"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []

View File

@ -0,0 +1,12 @@
../target/wasm32-unknown-unknown/release/near_token_bridge.wasm: *.rs ../Cargo.toml
(cd ..; cargo build --target wasm32-unknown-unknown --release)
.PHONY: clippy
clippy:
(cd ..; cargo clippy)
.PHONY: clippy-fix
clippy-fix:
(cd ..; cargo clippy --fix)

View File

@ -0,0 +1 @@
../../wormhole/src/byte_utils.rs

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1 @@
../../wormhole/src/state.rs

1209
near/contracts/wormhole/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,28 @@
[package]
name = "near-wormhole"
version = "0.1.0"
authors = ["Josh Siegel <jsiegel@jumptrading.com>"]
edition = "2022"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde = { version = "*", features = ["derive"] }
serde_json = "*"
near-sdk = { version = "4.0.0", features = ["unstable"] }
hex = { version = "0.4.3" }
bs58 = "*"
[profile.release]
codegen-units = 1
# Tell `rustc` to optimize for small code size.
opt-level = "s"
lto = true
debug = false
panic = "abort"
# Opt into extra safety checks on arithmetic operations https://stackoverflow.com/a/64136471/249801
overflow-checks = true
[workspace]
members = []

View File

@ -0,0 +1,13 @@
../target/wasm32-unknown-unknown/release/near_wormhole.wasm: *.rs ../Cargo.toml
(cd ..; cargo build --target wasm32-unknown-unknown --release)
.PHONY: clippy
clippy:
(cd ..; cargo clippy)
.PHONY: clippy-fix
clippy-fix:
(cd ..; cargo clippy --fix)

View File

@ -0,0 +1,95 @@
pub trait ByteUtils {
fn get_u8(&self, index: usize) -> u8;
fn get_u16(&self, index: usize) -> u16;
fn get_u32(&self, index: usize) -> u32;
fn get_u64(&self, index: usize) -> u64;
fn get_u128_be(&self, index: usize) -> u128;
/// High 128 then low 128
fn get_u256(&self, index: usize) -> (u128, u128);
// fn get_address(&self, index: usize) -> CanonicalAddr;
fn get_bytes32(&self, index: usize) -> &[u8];
fn get_bytes(&self, index: usize, bytes: usize) -> &[u8];
fn get_const_bytes<const N: usize>(&self, index: usize) -> [u8; N];
}
impl ByteUtils for &[u8] {
fn get_u8(&self, index: usize) -> u8 {
self[index]
}
fn get_u16(&self, index: usize) -> u16 {
let mut bytes: [u8; 16 / 8] = [0; 16 / 8];
bytes.copy_from_slice(&self[index..index + 2]);
u16::from_be_bytes(bytes)
}
fn get_u32(&self, index: usize) -> u32 {
let mut bytes: [u8; 32 / 8] = [0; 32 / 8];
bytes.copy_from_slice(&self[index..index + 4]);
u32::from_be_bytes(bytes)
}
fn get_u64(&self, index: usize) -> u64 {
let mut bytes: [u8; 64 / 8] = [0; 64 / 8];
bytes.copy_from_slice(&self[index..index + 8]);
u64::from_be_bytes(bytes)
}
fn get_u128_be(&self, index: usize) -> u128 {
let mut bytes: [u8; 128 / 8] = [0; 128 / 8];
bytes.copy_from_slice(&self[index..index + 128 / 8]);
u128::from_be_bytes(bytes)
}
fn get_u256(&self, index: usize) -> (u128, u128) {
(self.get_u128_be(index), self.get_u128_be(index + 128 / 8))
}
// fn get_address(&self, index: usize) -> CanonicalAddr {
// 32 bytes are reserved for addresses, but only the last 20 bytes are taken by the actual address
// CanonicalAddr::from(&self[index + 32 - 20..index + 32])
// }
fn get_bytes32(&self, index: usize) -> &[u8] {
&self[index..index + 32]
}
fn get_bytes(&self, index: usize, bytes: usize) -> &[u8] {
&self[index..index + bytes]
}
fn get_const_bytes<const N: usize>(&self, index: usize) -> [u8; N] {
let mut bytes: [u8; N] = [0; N];
bytes.copy_from_slice(&self[index..index + N]);
bytes
}
}
/// Left-pad a 20 byte address with 0s
//pub fn extend_address_to_32(addr: &CanonicalAddr) -> Vec<u8> {
// extend_address_to_32_array(addr).to_vec()
//}
//pub fn extend_address_to_32_array(addr: &CanonicalAddr) -> [u8; 32] {
// let mut v: Vec<u8> = vec![0; 12];
// v.extend(addr.as_slice());
// let mut result: [u8; 32] = [0; 32];
// result.copy_from_slice(&v);
// result
//}
/// Turn a string into a fixed length array. If the string is shorter than the
/// resulting array, it gets padded with \0s on the right. If longer, it gets
/// truncated.
pub fn string_to_array<const N: usize>(s: &str) -> [u8; N] {
let bytes = s.as_bytes();
let len = usize::min(N, bytes.len());
let zeros = vec![0; N - len];
let padded = [bytes[..len].to_vec(), zeros].concat();
let mut result: [u8; N] = [0; N];
result.copy_from_slice(&padded);
result
}
pub fn extend_string_to_32(s: &str) -> Vec<u8> {
string_to_array::<32>(s).to_vec()
}
pub fn get_string_from_32(v: &[u8]) -> String {
let s = String::from_utf8_lossy(v);
s.chars().filter(|c| c != &'\0').collect()
}

View File

@ -0,0 +1,612 @@
use {
near_sdk::{
borsh::{
self,
BorshDeserialize,
BorshSerialize,
},
collections::{
LookupMap,
UnorderedSet,
},
env,
near_bindgen,
require,
AccountId,
Balance,
Gas,
Promise,
PromiseOrValue,
PublicKey,
},
serde::Serialize,
};
pub mod byte_utils;
pub mod state;
use crate::byte_utils::{
get_string_from_32,
ByteUtils,
};
const CHAIN_ID_NEAR: u16 = 15;
const CHAIN_ID_SOL: u16 = 1;
#[derive(BorshDeserialize, BorshSerialize)]
pub struct GuardianAddress {
pub bytes: Vec<u8>,
}
#[derive(BorshDeserialize, BorshSerialize)]
pub struct GuardianSetInfo {
pub addresses: Vec<GuardianAddress>,
pub expiration_time: u64, // Guardian set expiration time
}
impl GuardianSetInfo {
pub fn quorum(&self) -> usize {
((self.addresses.len() * 10 / 3) * 2) / 10 + 1
}
}
#[must_use]
#[derive(Serialize, Debug, Clone)]
pub struct WormholeEvent {
standard: String,
event: String,
data: String,
nonce: u32,
emitter: String,
seq: u64,
block: u64,
}
impl WormholeEvent {
fn to_json_string(&self) -> String {
// Events cannot fail to serialize so fine to panic on error
#[allow(clippy::redundant_closure)]
serde_json::to_string(self)
.ok()
.unwrap_or_else(|| env::abort())
}
fn to_json_event_string(&self) -> String {
format!("EVENT_JSON:{}", self.to_json_string())
}
/// Logs the event to the host. This is required to ensure that the event is triggered
/// and to consume the event.
pub(crate) fn emit(self) {
near_sdk::env::log_str(&self.to_json_event_string());
}
}
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct OldWormhole {
guardians: LookupMap<u32, GuardianSetInfo>,
dups: UnorderedSet<Vec<u8>>,
emitters: LookupMap<String, u64>,
guardian_set_expirity: u64,
guardian_set_index: u32,
owner_pk: PublicKey,
upgrade_hash: Vec<u8>,
message_fee: u128,
bank: u128,
}
#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize)]
pub struct Wormhole {
guardians: LookupMap<u32, GuardianSetInfo>,
dups: UnorderedSet<Vec<u8>>,
emitters: LookupMap<String, u64>,
guardian_set_expirity: u64,
guardian_set_index: u32,
owner_pk: PublicKey,
upgrade_hash: Vec<u8>,
message_fee: u128,
bank: u128,
}
impl Default for Wormhole {
fn default() -> Self {
Self {
guardians: LookupMap::new(b"gs".to_vec()),
dups: UnorderedSet::new(b"d".to_vec()),
emitters: LookupMap::new(b"e".to_vec()),
guardian_set_index: u32::MAX,
guardian_set_expirity: 24 * 60 * 60 * 1_000_000_000, // 24 hours in nanoseconds
owner_pk: env::signer_account_pk(),
upgrade_hash: b"".to_vec(),
message_fee: 0,
bank: 0,
}
}
}
// Nothing is mutable...
fn parse_and_verify_vaa(storage: &Wormhole, data: &[u8]) -> state::ParsedVAA {
let vaa = state::ParsedVAA::parse(data);
if vaa.version != 1 {
env::panic_str("InvalidVersion");
}
let guardian_set = storage
.guardians
.get(&vaa.guardian_set_index)
.expect("InvalidGuardianSetIndex");
if guardian_set.expiration_time != 0 && guardian_set.expiration_time < env::block_timestamp() {
env::panic_str("GuardianSetExpired");
}
if (vaa.len_signers as usize) < guardian_set.quorum() {
env::panic_str("ContractError");
}
// Lets calculate the digest that we are comparing against
let mut pos =
state::ParsedVAA::HEADER_LEN + (vaa.len_signers * state::ParsedVAA::SIGNATURE_LEN); // SIGNATURE_LEN: usize = 66;
let p1 = env::keccak256(&data[pos..]);
let digest = env::keccak256(&p1);
// Verify guardian signatures
let mut last_index: i32 = -1;
pos = state::ParsedVAA::HEADER_LEN; // HEADER_LEN: usize = 6;
for _ in 0..vaa.len_signers {
// which guardian signature is this?
let index = data.get_u8(pos) as i32;
// We can't go backwards or use the same guardian over again
if index <= last_index {
env::panic_str("WrongGuardianIndexOrder");
}
last_index = index;
pos += 1; // walk forward
// Grab the whole signature
let signature = &data[(pos)..(pos + state::ParsedVAA::SIG_DATA_LEN)]; // SIG_DATA_LEN: usize = 64;
let key = guardian_set.addresses.get(index as usize).unwrap();
pos += state::ParsedVAA::SIG_DATA_LEN; // SIG_DATA_LEN: usize = 64;
let recovery = data.get_u8(pos);
let v = env::ecrecover(&digest, signature, recovery, true).expect("cannot recover key");
let k = &env::keccak256(&v)[12..32];
if k != key.bytes {
env::log_str(&format!(
"wormhole/{}#{}: signature_error: {} != {}",
file!(),
line!(),
hex::encode(&k),
hex::encode(&key.bytes),
));
env::panic_str("GuardianSignatureError");
}
pos += 1;
}
vaa
}
fn vaa_update_contract(
storage: &mut Wormhole,
_vaa: &state::ParsedVAA,
data: &[u8],
deposit: Balance,
refund_to: AccountId,
) -> PromiseOrValue<bool> {
let chain = data.get_u16(33);
if chain != CHAIN_ID_NEAR {
env::panic_str("InvalidContractUpgradeChain");
}
let uh = data.get_bytes32(0);
env::log_str(&format!(
"wormhole/{}#{}: vaa_update_contract: {}",
file!(),
line!(),
hex::encode(&uh)
));
storage.upgrade_hash = uh.to_vec();
if deposit > 0 {
PromiseOrValue::Promise(Promise::new(refund_to).transfer(deposit))
} else {
PromiseOrValue::Value(true)
}
}
fn vaa_update_guardian_set(
storage: &mut Wormhole,
_vaa: &state::ParsedVAA,
data: &[u8],
mut deposit: Balance,
refund_to: AccountId,
) -> PromiseOrValue<bool> {
const ADDRESS_LEN: usize = 20;
let new_guardian_set_index = data.get_u32(0);
if storage.guardian_set_index + 1 != new_guardian_set_index {
env::panic_str("InvalidGovernanceSetIndex");
}
let n_guardians = data.get_u8(4);
let mut addresses = vec![];
for i in 0..n_guardians {
let pos = 5 + (i as usize) * ADDRESS_LEN;
addresses.push(GuardianAddress {
bytes: data[pos..pos + ADDRESS_LEN].to_vec(),
});
}
let guardian_set = &mut storage
.guardians
.get(&storage.guardian_set_index)
.expect("InvalidPreviousGuardianSetIndex");
guardian_set.expiration_time = env::block_timestamp() + storage.guardian_set_expirity;
storage
.guardians
.insert(&storage.guardian_set_index, guardian_set);
let g = GuardianSetInfo {
addresses,
expiration_time: 0,
};
let storage_used = env::storage_usage();
storage.guardians.insert(&new_guardian_set_index, &g);
storage.guardian_set_index = new_guardian_set_index;
let required_cost =
(Balance::from(env::storage_usage() - storage_used)) * env::storage_byte_cost();
if required_cost > deposit {
env::panic_str("DepositUnderflowForGuardianSet");
}
deposit -= required_cost;
if deposit > 0 {
PromiseOrValue::Promise(Promise::new(refund_to).transfer(deposit))
} else {
PromiseOrValue::Value(true)
}
}
fn handle_set_fee(
storage: &mut Wormhole,
_vaa: &state::ParsedVAA,
payload: &[u8],
deposit: Balance,
refund_to: AccountId,
) -> PromiseOrValue<bool> {
let (_, amount) = payload.get_u256(0);
storage.message_fee = amount as u128;
if deposit > 0 {
PromiseOrValue::Promise(Promise::new(refund_to).transfer(deposit))
} else {
PromiseOrValue::Value(true)
}
}
fn handle_transfer_fee(
storage: &mut Wormhole,
_vaa: &state::ParsedVAA,
payload: &[u8],
deposit: Balance,
) -> PromiseOrValue<bool> {
let (_, amount) = payload.get_u256(0);
let destination = payload.get_bytes32(32).to_vec();
if amount > storage.bank {
env::panic_str("bankUnderFlow");
}
// We only support addresses 32 bytes or shorter... No, we don't
// support hash addresses in this governance message
let d = AccountId::new_unchecked(get_string_from_32(&destination));
if (deposit + amount) > 0 {
storage.bank -= amount;
PromiseOrValue::Promise(Promise::new(d).transfer(deposit + amount))
} else {
PromiseOrValue::Value(true)
}
}
#[near_bindgen]
impl Wormhole {
// I like passing the vaa's as strings around since it will show
// up better in explorers... I'll let a near sensai talk me out
// of this...
pub fn verify_vaa(&self, vaa: String) -> u32 {
let g1 = env::used_gas();
let h = hex::decode(vaa).expect("invalidVaa");
parse_and_verify_vaa(self, &h);
let g2 = env::used_gas();
env::log_str(&format!(
"wormhole/{}#{}: vaa_verify: {}",
file!(),
line!(),
serde_json::to_string(&(g2 - g1)).unwrap()
));
self.guardian_set_index as u32
}
#[payable]
pub fn register_emitter(&mut self, emitter: String) -> PromiseOrValue<bool> {
let storage_used = env::storage_usage();
self.emitters.insert(&emitter, &1);
if env::storage_usage() < storage_used {
env::panic_str("ImpossibleStorage");
}
let required_cost =
(Balance::from(env::storage_usage() - storage_used)) * env::storage_byte_cost();
let mut deposit = env::attached_deposit();
if required_cost > deposit {
env::panic_str("DepositUnderflowForToken2");
}
deposit -= required_cost;
if deposit > 0 {
PromiseOrValue::Promise(Promise::new(env::predecessor_account_id()).transfer(deposit))
} else {
PromiseOrValue::Value(false)
}
}
#[payable]
pub fn publish_message(&mut self, data: String, nonce: u32) -> u64 {
require!(
env::prepaid_gas() >= Gas(10_000_000_000_000),
&format!(
"wormhole/{}#{}: more gas is required {}",
file!(),
line!(),
serde_json::to_string(&env::prepaid_gas()).unwrap()
)
);
require!(
env::attached_deposit() >= self.message_fee,
"message_fee not provided"
);
self.bank += env::attached_deposit();
let s = env::predecessor_account_id().to_string();
if !self.emitters.contains_key(&s) {
env::panic_str("EmitterNotRegistered");
}
let seq = self.emitters.get(&s).unwrap();
self.emitters.insert(&s, &(seq + 1));
WormholeEvent {
standard: "wormhole".to_string(),
event: "publish".to_string(),
data,
nonce,
emitter: hex::encode(env::sha256(s.as_bytes())),
seq,
block: env::block_height(),
}
.emit();
seq
}
#[payable]
pub fn submit_vaa(&mut self, vaa: String) -> PromiseOrValue<bool> {
let refund_to = env::predecessor_account_id();
let mut deposit = env::attached_deposit();
if env::attached_deposit() == 0 {
env::panic_str("PayForStorage");
}
if env::prepaid_gas() < Gas(150_000_000_000_000) {
env::panic_str("NotEnoughGas");
}
let h = hex::decode(vaa).expect("invalidVaa");
let vaa = parse_and_verify_vaa(self, &h);
// Check if VAA with this hash was already accepted
if self.dups.contains(&vaa.hash) {
env::panic_str("alreadyExecuted");
}
let storage_used = env::storage_usage();
self.dups.insert(&vaa.hash);
let required_cost =
(Balance::from(env::storage_usage() - storage_used)) * env::storage_byte_cost();
if required_cost > deposit {
env::panic_str("DepositUnderflowForDupSuppression");
}
deposit -= required_cost;
if (CHAIN_ID_SOL != vaa.emitter_chain)
|| (hex::decode("0000000000000000000000000000000000000000000000000000000000000004")
.unwrap()
!= vaa.emitter_address)
{
env::panic_str("InvalidGovernanceEmitter");
}
// This is the core contract... it SHOULD only get governance packets and be on the latest
if self.guardian_set_index != vaa.guardian_set_index {
env::panic_str("InvalidGovernanceSet");
}
let data: &[u8] = &vaa.payload;
if data[0..32]
!= hex::decode("00000000000000000000000000000000000000000000000000000000436f7265")
.unwrap()
{
env::panic_str("InvalidGovernanceModule");
}
let chain = data.get_u16(33);
let action = data.get_u8(32);
if !((action == 2 && chain == 0) || chain == CHAIN_ID_NEAR) {
env::panic_str("InvalidGovernanceChain");
}
let payload = &data[35..];
match action {
1u8 => vaa_update_contract(self, &vaa, payload, deposit, refund_to),
2u8 => vaa_update_guardian_set(self, &vaa, payload, deposit, refund_to),
3u8 => handle_set_fee(self, &vaa, payload, deposit, refund_to),
4u8 => handle_transfer_fee(self, &vaa, payload, deposit),
_ => env::panic_str("InvalidGovernanceAction"),
}
}
pub fn message_fee(&self) -> u128 {
self.message_fee
}
pub fn boot_wormhole(&mut self, gset: u32, addresses: Vec<String>) {
if self.owner_pk != env::signer_account_pk() {
env::panic_str("invalidSigner");
}
assert!(self.guardian_set_index == u32::MAX);
let addr = addresses
.iter()
.map(|address| GuardianAddress {
bytes: hex::decode(address).unwrap(),
})
.collect::<Vec<GuardianAddress>>();
let g = GuardianSetInfo {
addresses: addr,
expiration_time: 0,
};
self.guardians.insert(&gset, &g);
self.guardian_set_index = gset;
env::log_str(&format!("Booting guardian_set_index {}", gset));
}
#[private]
pub fn update_contract_done(
&mut self,
refund_to: near_sdk::AccountId,
storage_used: u64,
attached_deposit: u128,
) {
let delta = (env::storage_usage() as i128 - storage_used as i128)
* env::storage_byte_cost() as i128;
let refund = attached_deposit as i128 - delta;
if refund > 0 {
env::log_str(&format!(
"wormhole/{}#{}: update_contract_done: refund {} to {}",
file!(),
line!(),
refund,
refund_to
));
Promise::new(refund_to).transfer(refund as u128);
}
}
#[private]
fn update_contract_work(&mut self, v: Vec<u8>) -> Promise {
if env::attached_deposit() == 0 {
env::panic_str("attach some cash");
}
let s = env::sha256(&v);
env::log_str(&format!(
"wormhole/{}#{}: update_contract: {}",
file!(),
line!(),
hex::encode(&s)
));
if s.to_vec() != self.upgrade_hash {
env::panic_str("invalidUpgradeContract");
}
Promise::new(env::current_account_id())
.deploy_contract(v.to_vec())
.then(Self::ext(env::current_account_id()).update_contract_done(
env::predecessor_account_id(),
env::storage_usage(),
env::attached_deposit(),
))
}
#[payable]
pub fn pass(&mut self) -> bool {
env::log_str(&format!("wormhole::pass {} {}", file!(), line!()));
true
}
#[init(ignore_state)]
#[payable]
pub fn migrate() -> Self {
// call migrate on self
if env::attached_deposit() != 1 {
env::panic_str("Need money");
}
let old_state: OldWormhole = env::state_read().expect("failed");
if env::signer_account_pk() != old_state.owner_pk {
env::panic_str("CannotCallMigrate");
}
env::log_str(&format!("wormhole/{}#{}: migrate", file!(), line!(),));
Self {
guardians: old_state.guardians,
dups: old_state.dups,
emitters: old_state.emitters,
guardian_set_expirity: old_state.guardian_set_expirity,
guardian_set_index: old_state.guardian_set_index,
owner_pk: old_state.owner_pk,
upgrade_hash: old_state.upgrade_hash,
message_fee: old_state.message_fee,
bank: old_state.bank,
}
}
}
// let result = await userAccount.functionCall({
// contractId: config.wormholeAccount,
// methodName: "update_contract",
// args: await fs.readFileSync("...."),
// attachedDeposit: "12500000000000000000000",
// gas: 300000000000000,
// });
#[no_mangle]
pub extern "C" fn update_contract() {
env::setup_panic_hook();
let mut contract: Wormhole = env::state_read().expect("Contract is not initialized");
contract.update_contract_work(env::input().unwrap());
}

View File

@ -0,0 +1,111 @@
use {
crate::byte_utils::ByteUtils,
near_sdk::env,
};
// Validator Action Approval(VAA) data
pub struct ParsedVAA {
pub version: u8,
pub guardian_set_index: u32,
pub timestamp: u32,
pub nonce: u32,
pub len_signers: usize,
pub emitter_chain: u16,
pub emitter_address: Vec<u8>,
pub sequence: u64,
pub consistency_level: u8,
pub payload: Vec<u8>,
pub hash: Vec<u8>,
}
impl ParsedVAA {
/* VAA format:
header (length 6):
0 uint8 version (0x01)
1 uint32 guardian set index
5 uint8 len signatures
per signature (length 66):
0 uint8 index of the signer (in guardian keys)
1 [65]uint8 signature
body:
0 uint32 timestamp (unix in seconds)
4 uint32 nonce
8 uint16 emitter_chain
10 [32]uint8 emitter_address
42 uint64 sequence
50 uint8 consistency_level
51 []uint8 payload
*/
pub const HEADER_LEN: usize = 6;
pub const SIGNATURE_LEN: usize = 66;
pub const GUARDIAN_SET_INDEX_POS: usize = 1;
pub const LEN_SIGNER_POS: usize = 5;
pub const VAA_NONCE_POS: usize = 4;
pub const VAA_EMITTER_CHAIN_POS: usize = 8;
pub const VAA_EMITTER_ADDRESS_POS: usize = 10;
pub const VAA_SEQUENCE_POS: usize = 42;
pub const VAA_CONSISTENCY_LEVEL_POS: usize = 50;
pub const VAA_PAYLOAD_POS: usize = 51;
// Signature data offsets in the signature block
pub const SIG_DATA_POS: usize = 1;
// Signature length minus recovery id at the end
pub const SIG_DATA_LEN: usize = 64;
// Recovery byte is last after the main signature
pub const SIG_RECOVERY_POS: usize = Self::SIG_DATA_POS + Self::SIG_DATA_LEN;
pub fn parse(data: &[u8]) -> Self {
let version = data.get_u8(0);
// Load 4 bytes starting from index 1
let guardian_set_index: u32 = data.get_u32(Self::GUARDIAN_SET_INDEX_POS);
let len_signers = data.get_u8(Self::LEN_SIGNER_POS) as usize;
let body_offset: usize = Self::HEADER_LEN + Self::SIGNATURE_LEN * len_signers as usize;
// Hash the body
if body_offset >= data.len() {
env::panic_str("InvalidVAA");
}
let body = &data[body_offset..];
let hash = env::keccak256(body);
// Signatures valid, apply VAA
if body_offset + Self::VAA_PAYLOAD_POS > data.len() {
env::panic_str("InvalidVAA");
}
let timestamp = data.get_u32(body_offset);
let nonce = data.get_u32(body_offset + Self::VAA_NONCE_POS);
let emitter_chain = data.get_u16(body_offset + Self::VAA_EMITTER_CHAIN_POS);
let emitter_address = data
.get_bytes32(body_offset + Self::VAA_EMITTER_ADDRESS_POS)
.to_vec();
let sequence = data.get_u64(body_offset + Self::VAA_SEQUENCE_POS);
let consistency_level = data.get_u8(body_offset + Self::VAA_CONSISTENCY_LEVEL_POS);
let payload = data[body_offset + Self::VAA_PAYLOAD_POS..].to_vec();
ParsedVAA {
version,
guardian_set_index,
timestamp,
nonce,
len_signers: len_signers as usize,
emitter_chain,
emitter_address,
sequence,
consistency_level,
payload,
hash,
}
}
}

3
near/devnet_deploy.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash -f
npx ts-node devnet_deploy.ts

346
near/devnet_deploy.ts Normal file
View File

@ -0,0 +1,346 @@
// npx pretty-quick
const nearAPI = require("near-api-js");
const BN = require("bn.js");
const fs = require("fs");
const fetch = require("node-fetch");
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
function getConfig(env: any) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
wormholeAccount: "wormhole.test.near",
tokenAccount: "token.test.near",
nftAccount: "nft.test.near",
testAccount: "test.test.near",
};
case "testnet":
return {
networkId: "testnet",
nodeUrl: "https://rpc.testnet.near.org",
masterAccount: "wormhole.testnet",
wormholeAccount: "wormhole.wormhole.testnet",
tokenAccount: "token.wormhole.testnet",
nftAccount: "nft.wormhole.testnet",
testAccount: "test.wormhole.testnet",
};
}
return {};
}
async function initNear() {
let e = process.env.NEAR_ENV || "sandbox";
let config = getConfig(e);
let masterKey: any;
if (e === "sandbox") {
// Retrieve the validator key directly in the Tilt environment
const response = await fetch("http://localhost:3031/validator_key.json");
const keyFile = await response.json();
masterKey = nearAPI.utils.KeyPair.fromString(
keyFile.secret_key || keyFile.private_key
);
} else {
masterKey = nearAPI.utils.KeyPair.fromString(process.env.NEAR_PK);
}
let masterPubKey = masterKey.getPublicKey();
let keyStore = new nearAPI.keyStores.InMemoryKeyStore();
keyStore.setKey(config.networkId, config.masterAccount, masterKey);
let near = await nearAPI.connect({
deps: {
keyStore,
},
networkId: config.networkId,
nodeUrl: config.nodeUrl,
});
let masterAccount = new nearAPI.Account(
near.connection,
config.masterAccount
);
console.log(
"Finish init NEAR masterAccount: " +
JSON.stringify(await masterAccount.getAccountBalance())
);
const wormholeContract = await fs.readFileSync("./near_wormhole.wasm");
const tokenContract = await fs.readFileSync("./near_token_bridge.wasm");
const nftContract = await fs.readFileSync("./near_nft_bridge.wasm");
const testContract = await fs.readFileSync("./near_mock_bridge_integration.wasm");
let wormholeAccount: any;
console.log("setting key for new wormhole contract");
keyStore.setKey(config.networkId, config.wormholeAccount, masterKey);
keyStore.setKey(config.networkId, config.tokenAccount, masterKey);
keyStore.setKey(config.networkId, config.nftAccount, masterKey);
if (e === "sandbox") {
console.log("Deploying core/wormhole contract: " + config.wormholeAccount);
wormholeAccount = await masterAccount.createAndDeployContract(
config.wormholeAccount,
masterKey.getPublicKey(),
wormholeContract,
new BN("20000000000000000000000000")
);
await wormholeAccount.functionCall({
contractId: config.wormholeAccount,
methodName: "register_emitter",
args: {emitter: config.tokenAccount},
attachedDeposit: new BN("30000000000000000000000"),
gas: new BN("100000000000000"),
})
await wormholeAccount.functionCall({
contractId: config.wormholeAccount,
methodName: "register_emitter",
args: {emitter: config.testAccount},
attachedDeposit: new BN("30000000000000000000000"),
gas: new BN("100000000000000"),
})
await wormholeAccount.functionCall({
contractId: config.wormholeAccount,
methodName: "register_emitter",
args: {emitter: config.nftAccount},
attachedDeposit: new BN("30000000000000000000000"),
gas: new BN("100000000000000"),
})
} else {
// This uses the standard API to redeploy ... we can migrate over to the vaa's later
console.log(
"redeploying core/wormhole contract: " + config.wormholeAccount
);
wormholeAccount = new nearAPI.Account(
near.connection,
config.wormholeAccount
);
await wormholeAccount.deployContract(wormholeContract);
// console.log("migrating " + config.wormholeAccount);
// console.log(
// await wormholeAccount.functionCall({
// contractId: config.wormholeAccount,
// methodName: "migrate",
// args: {},
// attachedDeposit: new BN(1),
// gas: new BN("100000000000000"),
// })
// );
// console.log("done migrating " + config.tokenAccount);
}
let tokenAccount: any;
if (e === "sandbox") {
console.log("Deploying token bridgecontract: " + config.tokenAccount);
tokenAccount = await masterAccount.createAndDeployContract(
config.tokenAccount,
masterKey.getPublicKey(),
tokenContract,
new BN("20000000000000000000000000")
);
} else {
// This uses the standard API to redeploy ... we can migrate over to the vaa's later
console.log("redeploying token bridge contract: " + config.tokenAccount);
tokenAccount = new nearAPI.Account(near.connection, config.tokenAccount);
await tokenAccount.deployContract(tokenContract);
console.log( await tokenAccount.viewFunction(config.tokenAccount, "emitter", {}));
// console.log("migrating " + config.tokenAccount);
// console.log(
// await tokenAccount.functionCall({
// contractId: config.tokenAccount,
// methodName: "migrate",
// args: {},
// attachedDeposit: new BN(1),
// gas: new BN("100000000000000"),
// })
// );
// console.log("done migrating " + config.tokenAccount);
}
let nftAccount: any;
if (e === "sandbox") {
console.log("Deploying nft bridge contract: " + config.nftAccount);
let nftAccount = await masterAccount.createAndDeployContract(
config.nftAccount,
masterKey.getPublicKey(),
nftContract,
new BN("20000000000000000000000000")
);
} else {
// This uses the standard API to redeploy ... we can migrate over to the vaa's later
console.log("redeploying nft contract: " + config.nftAccount);
nftAccount = new nearAPI.Account(near.connection, config.nftAccount);
await nftAccount.deployContract(nftContract);
}
let lines: any;
if (e === "sandbox") {
console.log("Deploying mach contract to " + config.testAccount);
let testAccount = await masterAccount.createAndDeployContract(
config.testAccount,
masterKey.getPublicKey(),
testContract,
new BN("25000000000000000000000000")
);
console.log("booting wormhole to devnet keys");
lines = fs.readFileSync(".env", "utf-8").split("\n");
} else {
console.log("booting wormhole to testnet keys");
lines = fs.readFileSync("/home/jsiegel/testnet-env", "utf-8").split("\n");
}
// console.log(lines);
let signers: any[] = [];
let vaasToken: any[] = [];
let vaasNFT: any[] = [];
lines.forEach((line: any) => {
let f = line.split("=");
if (f[0] === "INIT_SIGNERS") {
signers = eval(f[1]);
}
if (f[0].startsWith("REGISTER_") && f[0].endsWith("TOKEN_BRIDGE_VAA")) {
vaasToken.push(f[1]);
} else if (f[0].endsWith("TOKEN_BRIDGE_VAA_REGISTER")) {
vaasToken.push(f[1]);
}
if (f[0].startsWith("REGISTER_") && f[0].endsWith("NFT_BRIDGE_VAA")) {
vaasNFT.push(f[1]);
} else if (
f[0].endsWith("NFT_BRIDGE_VAA") ||
f[0].endsWith("NFT_BRIDGE_VAA_REGISTER")
) {
vaasNFT.push(f[1]);
}
});
console.log(vaasToken);
console.log(vaasNFT);
if (e === "sandbox") {
let result = await masterAccount.functionCall({
contractId: config.wormholeAccount,
methodName: "boot_wormhole",
args: {
gset: 0,
addresses: signers,
},
gas: 100000000000000,
});
console.log("Booting up the token bridge");
result = await masterAccount.functionCall({
contractId: config.tokenAccount,
methodName: "boot_portal",
args: {
core: config.wormholeAccount,
},
gas: 100000000000000,
});
console.log("Booting up the nft bridge");
result = await masterAccount.functionCall({
contractId: config.nftAccount,
methodName: "boot_portal",
args: {
core: config.wormholeAccount,
},
gas: 100000000000000,
});
}
// for (const line of vaasNFT) {
// console.log("Submitting to " + config.nftAccount + ": " + line);
//
// try {
// await masterAccount.functionCall({
// contractId: config.nftAccount,
// methodName: "submit_vaa",
// args: {
// vaa: line,
// },
// attachedDeposit: new BN("30000000000000000000000"),
// gas: new BN("300000000000000"),
// });
//
// await masterAccount.functionCall({
// contractId: config.nftAccount,
// methodName: "submit_vaa",
// args: {
// vaa: line,
// },
// attachedDeposit: new BN("30000000000000000000000"),
// gas: new BN("300000000000000"),
// });
// } catch {
// console.log("Exception thrown.. ");
// }
// }
console.log("nft bridge booted");
for (const line of vaasToken) {
console.log("Submitting to " + config.tokenAccount + ": " + line);
try {
await masterAccount.functionCall({
contractId: config.tokenAccount,
methodName: "submit_vaa",
args: {
vaa: line,
},
attachedDeposit: new BN("30000000000000000000001"),
gas: new BN("300000000000000"),
});
await masterAccount.functionCall({
contractId: config.tokenAccount,
methodName: "submit_vaa",
args: {
vaa: line,
},
attachedDeposit: new BN("30000000000000000000001"),
gas: new BN("300000000000000"),
});
} catch {
console.log("Exception thrown.. ");
}
}
console.log("token bridge booted");
// console.log("deleting the master key from the token contract");
// await tokenAccount.deleteKey(masterKey.getPublicKey());
// console.log("deleting the master key from the nft contract");
// await nftAccount.deleteKey(masterKey.getPublicKey());
// console.log("deleting the master key from the wormhole contract");
// await wormholeAccount.deleteKey(masterKey.getPublicKey());
}
initNear();

11
near/node_builder.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/bash
apt-get update
apt-get -y install libclang-dev jq
git clone https://github.com/near/nearcore
cd nearcore
make sandbox-release

26991
near/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
near/package.json Normal file
View File

@ -0,0 +1,46 @@
{
"private": true,
"name": "wormhole-near",
"version": "1.0.0",
"description": "Wormhole near support code",
"keywords": [
"near-protocol",
"wormhole",
"rust",
"smart-contract"
],
"author": "Josh Siegel",
"dependencies": {
"@certusone/wormhole-sdk": "file:../sdk/js",
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@openzeppelin/contracts": "^4.6.0",
"big.js": "^5.2.2",
"bn.js": "^5.2.0",
"borsh": "^0.4.0",
"bs58": "^5.0.0",
"env-cmd": "^10.1.0",
"ethers": "^5.6.5",
"near-api-js": "^0.43.1",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"regenerator-runtime": "^0.13.9",
"ts-node": "^10.8.1",
"web3": "^1.7.4",
"web3-eth-abi": "^1.7.3",
"web3-utils": "^1.7.3"
},
"devDependencies": {
"@babel/core": "^7.14.0",
"@babel/preset-env": "^7.14.2",
"@babel/preset-react": "^7.13.13",
"cssnano": "^5.0.7",
"gh-pages": "^3.1.0",
"parcel-bundler": "^1.12.5",
"postcss": "^8.3.6",
"sass": "^1.37.0"
},
"resolutions": {
"@babel/preset-env": "7.13.8"
}
}

4
near/rust-toolchain.toml Normal file
View File

@ -0,0 +1,4 @@
[toolchain]
channel = "nightly"
targets = [ "wasm32-unknown-unknown" ]
profile = "default"

17
near/rustfmt.toml Normal file
View File

@ -0,0 +1,17 @@
# Merge all imports into a clean vertical list of module imports.
imports_granularity = "One"
group_imports = "One"
imports_layout = "Vertical"
# Better grep-ability.
empty_item_single_line = false
# Consistent pipe layout.
match_arm_leading_pipes = "Preserve"
# Align Fields
enum_discrim_align_threshold = 80
struct_field_align_threshold = 80
# Allow up to two blank lines for visual grouping.
blank_lines_upper_bound = 2

7
near/setup-rust.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source $HOME/.cargo/env
rustup default 1.60.0
rustup update
rustup target add wasm32-unknown-unknown

15
near/start_node.sh Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
set -x
echo "starting web server"
python3 -m http.server --directory /tmp/_sandbox 3031 &
echo "starting near sandbox"
rm -rf /tmp/_sandbox
nearcore/target/release/near-sandbox --home /tmp/_sandbox init
tmpfile="$(mktemp)"
jq '.store.max_open_files = 9000' /tmp/_sandbox/config.json > "$tmpfile"
cp "$tmpfile" /tmp/_sandbox/config.json
nearcore/target/release/near-sandbox --home /tmp/_sandbox run

435
near/test/algoHelpers.ts Normal file
View File

@ -0,0 +1,435 @@
import algosdk, {
Account,
Algodv2,
assignGroupID,
makePaymentTxnWithSuggestedParamsFromObject,
Transaction,
waitForConfirmation,
} from "@certusone/wormhole-sdk/node_modules/algosdk";
import { getForeignAssetAlgorand } from "@certusone/wormhole-sdk/lib/cjs/token_bridge";
import { ChainId } from "@certusone/wormhole-sdk/lib/cjs/utils";
import {
TransactionSignerPair,
_parseVAAAlgorand,
} from "@certusone/wormhole-sdk/lib/cjs/algorand";
const ci = !!process.env.CI;
const ALGO_TOKEN =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const ALGOD_ADDRESS: string = ci ? "http://algorand" : "http://localhost";
const ALGOD_PORT: number = 4001;
/**
* Creates a new Algodv2 client using local file consts
* @returns a newly constructed Algodv2 client
*/
export function getAlgoClient(): Algodv2 {
const algodClient = new Algodv2(ALGO_TOKEN, ALGOD_ADDRESS, ALGOD_PORT);
return algodClient;
}
let KMD_TOKEN =
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
const KMD_ADDRESS: string = ci ? "http://algorand" : "http://localhost";
const KMD_PORT: number = 4002;
const KMD_WALLET_NAME: string = "unencrypted-default-wallet";
const KMD_WALLET_PASSWORD: string = "";
export function getKmdClient(): algosdk.Kmd {
const kmdClient: algosdk.Kmd = new algosdk.Kmd(
KMD_TOKEN,
KMD_ADDRESS,
KMD_PORT
);
return kmdClient;
}
export async function getGenesisAccounts(): Promise<Account[]> {
let retval: Account[] = [];
const kmd: algosdk.Kmd = getKmdClient();
// Get list of wallets
const wallets = (await kmd.listWallets()).wallets;
if (!wallets) {
console.error("No wallets found!");
return retval;
}
// Walk walles to find correct wallet
let myWalletId: string = "";
wallets.forEach((element: any) => {
// console.log(element);
if (element.name === KMD_WALLET_NAME) {
myWalletId = element.id;
}
});
if (myWalletId.length === 0) {
console.error("invalid wallet ID");
return retval;
}
// Get the wallet handle for Genesis wallet
const myWalletHandle = (
await kmd.initWalletHandle(myWalletId, KMD_WALLET_PASSWORD)
).wallet_handle_token;
// console.log("myWalletHandle:", myWalletHandle);
// Get the 3 addresses associated with the Genesis wallet
const addresses = (await kmd.listKeys(myWalletHandle)).addresses;
// console.log("addresses:", addresses);
for (let i = 0; i < addresses.length; i++) {
const element = addresses[i];
const myExportedKey: Buffer = (
await kmd.exportKey(myWalletHandle, KMD_WALLET_PASSWORD, element)
).private_key;
// console.log("exported key:", element, myExportedKey);
let mn = algosdk.secretKeyToMnemonic(myExportedKey);
let ta = algosdk.mnemonicToSecretKey(mn);
retval.push(ta);
}
kmd.releaseWalletHandle(myWalletHandle);
// console.log("length of genesis accounts:", retval.length);
return retval;
}
export async function firstKmdTransaction() {
try {
const genAccounts = await getGenesisAccounts();
// const walletRsp = await myKmdClient.getWallet(myWalletHandle);
// console.log("walletRsp:", walletRsp);
} catch (e) {
console.log("KMD transaction error:", e);
}
}
// This function creates temporary accounts and funds them with the
// Genesis accounts.
export async function getTempAccounts(): Promise<Account[]> {
let retval: Account[] = [];
const algodClient = getAlgoClient();
const genesisAccounts: Account[] = await getGenesisAccounts();
const numAccts = genesisAccounts.length;
if (numAccts === 0) {
console.error("Failed to get genesisAccounts");
return retval;
}
// console.log("About to construct txns...");
const params = await algodClient.getTransactionParams().do();
let transactions: Transaction[] = [];
for (let i = 0; i < numAccts; i++) {
let newAcct = createAccount();
if (!newAcct) {
throw new Error("failed to create a temp account");
}
let fundingAcct = genesisAccounts[i];
// Create a payment transaction
// console.log(
// "Creating paytxn with fundAcct",
// fundingAcct,
// "newAcct",
// newAcct
// );
const payTxn = makePaymentTxnWithSuggestedParamsFromObject({
from: fundingAcct.addr,
to: newAcct.addr,
amount: 15000000,
suggestedParams: params,
});
// Sign the transaction
// console.log("signing paytxn...");
const signedTxn = payTxn.signTxn(fundingAcct.sk);
const signedTxnId = payTxn.txID().toString();
// console.log("signedTxnId:", signedTxnId);
// Submit the transaction
// console.log("submitting transaction...");
const txId = await algodClient.sendRawTransaction(signedTxn).do();
// console.log("submitted txId:", txId);
// Wait for response
const confirmedTxn = await algosdk.waitForConfirmation(
algodClient,
signedTxnId,
4
);
//Get the completed Transaction
// console.log(
// "Transaction " +
// txId +
// " confirmed in round " +
// confirmedTxn["confirmed-round"]
// );
// console.log("Confirmation response:", confirmedTxn);
// let mytxinfo = JSON.stringify(confirmedTxn.txn.txn, undefined, 2);
// console.log("Transaction information: %o", mytxinfo);
// let string = new TextDecoder().decode(confirmedTxn.txn.txn.note);
// console.log("Note field: ", string);
let accountInfo = await algodClient.accountInformation(newAcct.addr).do();
// console.log(
// "Transaction Amount: %d microAlgos",
// confirmedTxn.txn.txn.amt
// );
// console.log("Transaction Fee: %d microAlgos", confirmedTxn.txn.txn.fee);
// console.log("Account balance: %d microAlgos", accountInfo.amount);
retval.push(newAcct);
}
return retval;
}
export function createAccount(): Account | undefined {
try {
const retval = algosdk.generateAccount();
// let retval = new Account(tempAcct.addr, Buffer.from(tempAcct.sk));
// let account_mnemonic = algosdk.secretKeyToMnemonic(tempAcct.sk);
// console.log("Account Address = " + retval.addr);
// console.log("Account Mnemonic = " + account_mnemonic);
// console.log("Class Account:", retval);
return retval;
} catch (err) {
console.log("err", err);
}
}
/**
* Return the balances of all assets for the supplied account
* @param client An Algodv2 client
* @param account The account containing assets
* @returns Map of asset index to qty
*/
export async function getBalances(
client: Algodv2,
account: string
): Promise<Map<number, number>> {
let balances = new Map<number, number>();
const accountInfo = await client.accountInformation(account).do();
console.log("Account Info:", accountInfo);
console.log("Account Info|created-assets:", accountInfo["created-assets"]);
// Put the algo balance in key 0
balances.set(0, accountInfo.amount);
const assets: Array<any> = accountInfo.assets;
console.log("assets", assets);
assets.forEach(function (asset) {
console.log("inside foreach", asset);
const assetId = asset["asset-id"];
const amount = asset.amount;
balances.set(assetId, amount);
});
console.log("balances", balances);
return balances;
}
/**
* Return the balance of the supplied asset index for the supplied account
* @param client An Algodv2 client
* @param account The account to query for the supplied asset index
* @param assetId The asset index
* @returns The quantity of the asset in the supplied account
*/
export async function getBalance(
client: Algodv2,
account: string,
assetId: bigint
): Promise<bigint> {
const accountInfo = await client.accountInformation(account).do();
if (assetId === BigInt(0)) {
return accountInfo.amount;
}
let ret = BigInt(0);
const assets: Array<any> = accountInfo.assets;
assets.forEach((asset) => {
if (Number(assetId) === asset["asset-id"]) {
ret = asset.amount;
return;
}
});
return ret;
}
export async function createAsset(account: Account): Promise<any> {
console.log("Creating asset...");
const aClient = getAlgoClient();
const params = await aClient.getTransactionParams().do();
const note = undefined; // arbitrary data to be stored in the transaction; here, none is stored
// Asset creation specific parameters
const addr = account.addr;
// Whether user accounts will need to be unfrozen before transacting
const defaultFrozen = false;
// integer number of decimals for asset unit calculation
const decimals = 10;
// total number of this asset available for circulation
const totalIssuance = 1000000;
// Used to display asset units to user
const unitName = "NORIUM";
// Friendly name of the asset
const assetName = "ChuckNorium";
// Optional string pointing to a URL relating to the asset
// const assetURL = "http://www.chucknorris.com";
const assetURL = "";
// Optional hash commitment of some sort relating to the asset. 32 character length.
// const assetMetadataHash = "16efaa3924a6fd9d3a4824799a4ac65d";
const assetMetadataHash = "";
// The following parameters are the only ones
// that can be changed, and they have to be changed
// by the current manager
// Specified address can change reserve, freeze, clawback, and manager
const manager = account.addr;
// Specified address is considered the asset reserve
// (it has no special privileges, this is only informational)
const reserve = account.addr;
// Specified address can freeze or unfreeze user asset holdings
const freeze = account.addr;
// Specified address can revoke user asset holdings and send
// them to other addresses
const clawback = account.addr;
// signing and sending "txn" allows "addr" to create an asset
const txn = algosdk.makeAssetCreateTxnWithSuggestedParams(
addr,
note,
totalIssuance,
decimals,
defaultFrozen,
manager,
reserve,
freeze,
clawback,
unitName,
assetName,
assetURL,
assetMetadataHash,
params
);
const rawSignedTxn = txn.signTxn(account.sk);
// console.log("rawSignedTxn:", rawSignedTxn);
const tx = await aClient.sendRawTransaction(rawSignedTxn).do();
// wait for transaction to be confirmed
const ptx = await algosdk.waitForConfirmation(aClient, tx.txId, 4);
// console.log("createAsset() - ptx:", ptx);
// Get the new asset's information from the creator account
const assetID: number = ptx["asset-index"];
//Get the completed Transaction
console.log(
"createAsset() - Transaction " +
tx.txId +
" confirmed in round " +
ptx["confirmed-round"]
);
return assetID;
}
export async function createNFT(account: Account): Promise<number> {
console.log("Creating NFT...");
const aClient = getAlgoClient();
const params = await aClient.getTransactionParams().do();
// Asset creation specific parameters
const addr = account.addr;
// Whether user accounts will need to be unfrozen before transacting
const defaultFrozen = false;
// integer number of decimals for asset unit calculation
const decimals = 0;
// total number of this asset available for circulation
const total = 1;
// Used to display asset units to user
const unitName = "CNART";
// Friendly name of the asset
const assetName = "ChuckNoriumArtwork@arc3";
// Optional string pointing to a URL relating to the asset
const assetURL = "http://www.chucknorris.com";
// Optional hash commitment of some sort relating to the asset. 32 character length.
const assetMetadataHash = "16efaa3924a6fd9d3a4824799a4ac65d";
// The following parameters are the only ones
// that can be changed, and they have to be changed
// by the current manager
// Specified address can change reserve, freeze, clawback, and manager
const manager = account.addr;
// Specified address is considered the asset reserve
// (it has no special privileges, this is only informational)
const reserve = account.addr;
// Specified address can freeze or unfreeze user asset holdings
const freeze = account.addr;
// Specified address can revoke user asset holdings and send
// them to other addresses
const clawback = account.addr;
// signing and sending "txn" allows "addr" to create an asset
const txn = algosdk.makeAssetCreateTxnWithSuggestedParamsFromObject({
from: addr,
total,
decimals,
assetName,
unitName,
assetURL,
assetMetadataHash,
defaultFrozen,
freeze,
manager,
clawback,
reserve,
suggestedParams: params,
});
const rawSignedTxn = txn.signTxn(account.sk);
// console.log("rawSignedTxn:", rawSignedTxn);
const tx = await aClient.sendRawTransaction(rawSignedTxn).do();
// wait for transaction to be confirmed
const ptx = await algosdk.waitForConfirmation(aClient, tx.txId, 4);
// Get the new asset's information from the creator account
const assetID: number = ptx["asset-index"];
console.log(
"createNFT() - Transaction " +
tx.txId +
" confirmed in round " +
ptx["confirmed-round"]
);
return assetID;
}
export async function getForeignAssetFromVaaAlgorand(
client: Algodv2,
tokenBridgeId: bigint,
vaa: Uint8Array
): Promise<bigint | null> {
const parsedVAA = _parseVAAAlgorand(vaa);
if (parsedVAA.Contract === undefined) {
throw "parsedVAA.Contract is undefined";
}
return await getForeignAssetAlgorand(
client,
tokenBridgeId,
parsedVAA.FromChain as ChainId,
parsedVAA.Contract
);
}
export async function signSendAndConfirmAlgorand(
algodClient: Algodv2,
txs: TransactionSignerPair[],
wallet: Account
) {
assignGroupID(txs.map((tx) => tx.tx));
const signedTxns: Uint8Array[] = [];
for (const tx of txs) {
if (tx.signer) {
signedTxns.push(await tx.signer.signTxn(tx.tx));
} else {
signedTxns.push(tx.tx.signTxn(wallet.sk));
}
}
await algodClient.sendRawTransaction(signedTxns).do();
const result = await waitForConfirmation(
algodClient,
txs[txs.length - 1].tx.txID(),
1
);
return result;
}

14
near/test/build.sh Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env bash
cd ../..
DOCKER_BUILDKIT=1 docker build --target go-export -f Dockerfile.proto -o type=local,dest=node .
DOCKER_BUILDKIT=1 docker build --target node-export -f Dockerfile.proto -o type=local,dest=. .
cd node/
echo "Have patience, this step takes upwards of 500 seconds!"
if [ $(uname -m) = "arm64" ]; then
echo "Building Guardian for linux/amd64"
DOCKER_BUILDKIT=1 docker build --platform linux/amd64 -f Dockerfile -t guardian .
else
echo "Building Guardian natively"
DOCKER_BUILDKIT=1 docker build -f Dockerfile -t guardian .
fi

112
near/test/devnet_upgrade.ts Normal file
View File

@ -0,0 +1,112 @@
const nearAPI = require("near-api-js");
const BN = require("bn.js");
const bs58 = require("bs58");
const fs = require("fs");
const fetch = require("node-fetch");
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
function getConfig(env: any) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
wormholeAccount: "wormhole.test.near",
tokenAccount: "token.test.near",
userAccount:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
};
}
return {};
}
async function testDeploy() {
let config = getConfig(process.env.NEAR_ENV || "sandbox");
// Retrieve the validator key directly in the Tilt environment
const response = await fetch("http://localhost:3031/validator_key.json");
const keyFile = await response.json();
let masterKey = nearAPI.utils.KeyPair.fromString(
keyFile.secret_key || keyFile.private_key
);
let masterPubKey = masterKey.getPublicKey();
let keyStore = new nearAPI.keyStores.InMemoryKeyStore();
keyStore.setKey(config.networkId, config.masterAccount, masterKey);
let near = await nearAPI.connect({
deps: {
keyStore,
},
networkId: config.networkId,
nodeUrl: config.nodeUrl,
});
let masterAccount = new nearAPI.Account(
near.connection,
config.masterAccount
);
console.log(
"Finish init NEAR masterAccount: " +
JSON.stringify(await masterAccount.getAccountBalance())
);
let userKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
keyStore.setKey(config.networkId, config.userAccount, userKey);
console.log(
"creating a user account: " +
config.userAccount +
" with key " +
userKey.getPublicKey()
);
await masterAccount.createAccount(
config.userAccount,
userKey.getPublicKey(),
new BN(10).pow(new BN(27))
);
const userAccount = new nearAPI.Account(near.connection, config.userAccount);
const wormholeContract = await fs.readFileSync(
"../contracts/wormhole/target/wasm32-unknown-unknown/release/near_wormhole.wasm"
);
//console.log("sending money to cover the cost of deploying this contract.. so that we fail for the right reasons");
//await userAccount.sendMoney(config.wormholeAccount, new BN("10000000000000000000000000"));
keyStore.setKey(config.networkId, config.wormholeAccount, masterKey);
let wormholeAccount = new nearAPI.Account(
near.connection,
config.wormholeAccount
);
try {
console.log("redeploying wormhole contract using standard deployment API");
let resp = await wormholeAccount.deployContract(wormholeContract);
console.log(resp);
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. nice.. we dont suck");
}
console.log("calling upgradeContract");
let result = await userAccount.functionCall({
contractId: config.wormholeAccount,
methodName: "update_contract",
args: wormholeContract,
attachedDeposit: "12500000000000000000000",
gas: 300000000000000,
});
console.log("done");
console.log(result);
}
testDeploy();

9
near/test/go.mod Normal file
View File

@ -0,0 +1,9 @@
module github.com/certusone/wormhole/near
go 1.17
require (
github.com/tidwall/gjson v1.14.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
)

6
near/test/go.sum Normal file
View File

@ -0,0 +1,6 @@
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/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=

39
near/test/key.js Normal file
View File

@ -0,0 +1,39 @@
let elliptic = require('elliptic');
const web3Utils = require("web3-utils");
const guardianKey = "beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe";
const guardianPrivKeys = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
let ec = new elliptic.ec('secp256k1');
let keyPair = ec.keyFromPrivate(guardianPrivKeys);
let privKey = keyPair.getPrivate("hex");
let pubKey = keyPair.getPublic();
console.log(`Private key: ${privKey}`);
console.log("Public key :", pubKey.encode("hex"))
//console.log("Public key (compressed):", pubKey.encodeCompressed("hex"));
let msg = 'Message for signing';
let msgHash = web3Utils.keccak256(msg).substr(2);
let signature = keyPair.sign(msgHash, { canonical: true });
console.log(`Msg: ${msg}`);
console.log(`Msg hash: ${msgHash}`);
console.log("Signature:", signature);
console.log();
let hexToDecimal = (x) => ec.keyFromPrivate(x, "hex").getPrivate().toString(10);
let pubKeyRecovered = ec.recoverPubKey(hexToDecimal(msgHash), signature, signature.recoveryParam, "hex");
pk = pubKeyRecovered.encode("hex")
console.log("Public key :", pubKey.encode("hex").substring(2, 130));
console.log("Recovered pubKey:", pk);
console.log("keccak256 :", web3Utils.keccak256("0x" + pk.substring(2, 130)).substring(26))
console.log("guardianKey:", guardianKey);
let validSig = ec.verify(msgHash, signature, pubKeyRecovered);
console.log("Signature valid?", validSig);

91
near/test/msg.ts Normal file
View File

@ -0,0 +1,91 @@
// npx pretty-quick
import Web3 from "web3";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { TestLib } from "./testlib";
import { Account as nearAccount } from "@certusone/wormhole-sdk/node_modules/near-api-js";
const BN = require("bn.js");
const fetch = require("node-fetch");
const nearAPI = require("near-api-js");
export function textToHexString(name: string): string {
return Buffer.from(name, "binary").toString("hex");
}
export const hexToUint8Array = (h: string): Uint8Array =>
new Uint8Array(Buffer.from(h, "hex"));
export const uint8ArrayToHex = (a: Uint8Array): string =>
Buffer.from(a).toString("hex");
function getConfig(env: any) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
wormholeAccount: "wormhole.test.near",
tokenAccount: "token.test.near",
nftAccount: "nft.test.near",
userAccount:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
user2Account:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
};
}
return {};
}
async function testNearMsg() {
let config = getConfig(process.env.NEAR_ENV || "sandbox");
// Retrieve the validator key directly in the Tilt environment
const response = await fetch("http://localhost:3031/validator_key.json");
const keyFile = await response.json();
let masterKey = nearAPI.utils.KeyPair.fromString(
keyFile.secret_key || keyFile.private_key
);
let masterPubKey = masterKey.getPublicKey();
let keyStore = new nearAPI.keyStores.InMemoryKeyStore();
keyStore.setKey(config.networkId, config.masterAccount, masterKey);
let near = await nearAPI.connect({
deps: {
keyStore,
},
networkId: config.networkId,
nodeUrl: config.nodeUrl,
});
let masterAccount = new nearAPI.Account(
near.connection,
config.masterAccount
);
console.log(
"Finish init NEAR masterAccount: " +
JSON.stringify(await masterAccount.getAccountBalance())
);
while (true) {
let myAddress = nearAPI.providers.getTransactionLastResult(
await masterAccount.functionCall({
contractId: "test.test.near",
methodName: "publish_message",
args: { core: "wormhole.test.near", p: "00" },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
})
);
await new Promise((f) => setTimeout(f, 1000));
}
}
testNearMsg();

350
near/test/nft.ts Normal file
View File

@ -0,0 +1,350 @@
// npx pretty-quick
import Web3 from "web3";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { TestLib } from "./testlib";
import {
NFTImplementation,
NFTImplementation__factory,
nft_bridge,
getEmitterAddressEth,
parseSequenceFromLogEth,
getSignedVAAWithRetry,
CHAIN_ID_ALGORAND,
CHAIN_ID_ETH,
CHAIN_ID_NEAR,
ChainId,
ChainName,
textToUint8Array,
tryNativeToUint8Array,
CONTRACTS,
parseNFTPayload,
} from "@certusone/wormhole-sdk";
import {
ParsedVAA,
_parseVAAAlgorand,
_parseNFTAlgorand,
} from "@certusone/wormhole-sdk/lib/cjs/algorand";
import {
BigNumberish,
ethers,
} from "@certusone/wormhole-sdk/node_modules/ethers";
import { Account as nearAccount } from "@certusone/wormhole-sdk/node_modules/near-api-js";
const BN = require("bn.js");
const fetch = require("node-fetch");
const ERC721 = require("@openzeppelin/contracts/build/contracts/ERC721PresetMinterPauserAutoId.json");
const nearAPI = require("near-api-js");
export const ETH_NODE_URL = "ws://localhost:8545";
export const ETH_PRIVATE_KEY =
"0x6cbed15c793ce57650b9877cf6fa156fbef513c4e6134f022a85b1ffdd59b2a1"; // account 1
export const ETH_CORE_BRIDGE_ADDRESS =
"0xC89Ce4735882C9F0f0FE26686c53074E09B0D550";
export const ETH_NFT_BRIDGE_ADDRESS =
"0x26b4afb60d6c903165150c6f0aa14f8016be4aec";
const web3 = new Web3(ETH_NODE_URL);
let provider: ethers.providers.WebSocketProvider =
new ethers.providers.WebSocketProvider(ETH_NODE_URL);
let signer: ethers.Wallet = new ethers.Wallet(ETH_PRIVATE_KEY, provider);
export function textToHexString(name: string): string {
return Buffer.from(name, "binary").toString("hex");
}
export const hexToUint8Array = (h: string): Uint8Array =>
new Uint8Array(Buffer.from(h, "hex"));
export const uint8ArrayToHex = (a: Uint8Array): string =>
Buffer.from(a).toString("hex");
async function _transferFromEth(
erc721: string,
token_id: BigNumberish,
address: string,
chain: ChainId
): Promise<ethers.ContractReceipt> {
return nft_bridge.transferFromEth(
ETH_NFT_BRIDGE_ADDRESS,
signer,
erc721,
token_id,
chain,
hexToUint8Array(address)
);
}
async function deployNFTOnEth(
name: string,
symbol: string,
uri: string,
how_many: number
): Promise<NFTImplementation> {
const accounts = await web3.eth.getAccounts();
const nftContract = new web3.eth.Contract(ERC721.abi);
let nft = await nftContract
.deploy({
data: ERC721.bytecode,
arguments: [name, symbol, uri],
})
.send({
from: accounts[1],
gas: 5000000,
});
// The eth contracts mints tokens with sequential ids, so in order to get to a
// specific id, we need to mint multiple nfts. We need this to test that
// foreign ids on terra get converted to the decimal stringified form of the
// original id.
for (var i = 0; i < how_many; i++) {
await nft.methods.mint(accounts[1]).send({
from: accounts[1],
gas: 1000000,
});
}
return NFTImplementation__factory.connect(nft.options.address, signer);
}
async function waitUntilEthTxObserved(
receipt: ethers.ContractReceipt
): Promise<Uint8Array> {
// get the sequence from the logs (needed to fetch the vaa)
let sequence = parseSequenceFromLogEth(receipt, ETH_CORE_BRIDGE_ADDRESS);
let emitterAddress = getEmitterAddressEth(ETH_NFT_BRIDGE_ADDRESS);
// poll until the guardian(s) witness and sign the vaa
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_ETH,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
return signedVAA;
}
function getConfig(env: any) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
wormholeAccount: "wormhole.test.near",
tokenAccount: "token.test.near",
nftAccount: "nft.test.near",
userAccount:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
user2Account:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
};
}
return {};
}
export const METADATA_REPLACE = new RegExp("\u0000", "g");
async function testNearSDK() {
let config = getConfig(process.env.NEAR_ENV || "sandbox");
// Retrieve the validator key directly in the Tilt environment
const response = await fetch("http://localhost:3031/validator_key.json");
const keyFile = await response.json();
let masterKey = nearAPI.utils.KeyPair.fromString(
keyFile.secret_key || keyFile.private_key
);
let masterPubKey = masterKey.getPublicKey();
let keyStore = new nearAPI.keyStores.InMemoryKeyStore();
keyStore.setKey(config.networkId, config.masterAccount, masterKey);
let near = await nearAPI.connect({
deps: {
keyStore,
},
networkId: config.networkId,
nodeUrl: config.nodeUrl,
});
let masterAccount = new nearAPI.Account(
near.connection,
config.masterAccount
);
console.log(
"Finish init NEAR masterAccount: " +
JSON.stringify(await masterAccount.getAccountBalance())
);
let userKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
keyStore.setKey(config.networkId, config.userAccount, userKey);
let user2Key = nearAPI.utils.KeyPair.fromRandom("ed25519");
keyStore.setKey(config.networkId, config.user2Account, user2Key);
console.log(
"creating a user account: " +
config.userAccount +
" with key " +
userKey.getPublicKey()
);
await masterAccount.createAccount(
config.userAccount,
userKey.getPublicKey(),
new BN(10).pow(new BN(27))
);
const userAccount = new nearAPI.Account(near.connection, config.userAccount);
console.log(
"Creating new random non-wormhole nft and air dropping it to myself"
);
let randoNFT = nearAPI.providers.getTransactionLastResult(
await userAccount.functionCall({
contractId: "test.test.near",
methodName: "deploy_nft",
args: {
account: userAccount.accountId,
},
attachedDeposit: new BN("10000000000000000000000000"),
gas: 300000000000000,
})
);
console.log(randoNFT);
console.log("Registering the user");
let myAddress = nearAPI.providers.getTransactionLastResult(
await userAccount.functionCall({
contractId: config.nftAccount,
methodName: "register_account",
args: { account: userAccount.accountId },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
})
);
console.log("myAddress: " + myAddress);
let contract = textToHexString(Math.random().toString());
let tokenid = textToHexString(Math.random().toString());
let ts = new TestLib();
let seq = Math.floor(new Date().getTime() / 1000);
let v = ts.genNFTTransfer(
ts.singleGuardianPrivKey,
0,
1,
seq,
contract,
1, // from chain
"George", // symbol
"GeorgesNFT", // name
tokenid,
"https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/10",
myAddress,
15
);
//let tvaa = ts.hexStringToUint8Array(v);
//console.log(v);
//console.log(_parseNFTAlgorand(tvaa));
let res = await userAccount.viewFunction(
config.nftAccount,
"deposit_estimates",
{}
);
console.log(res);
let emitter = (await userAccount.viewFunction(
config.nftAccount,
"emitter",
{}
))[1];
console.log(emitter);
console.log("submitting a nft");
let ret = nearAPI.providers.getTransactionLastResult(
await userAccount.functionCall({
contractId: config.nftAccount,
methodName: "submit_vaa",
args: {
vaa: v,
},
gas: 300000000000000,
attachedDeposit: new BN(res[1]),
})
);
console.log(ret);
ret = nearAPI.providers.getTransactionLastResult(
await userAccount.functionCall({
contractId: config.nftAccount,
methodName: "submit_vaa",
args: {
vaa: v,
},
gas: 300000000000000,
attachedDeposit: new BN(res[1]),
})
);
console.log(ret);
console.log("make it go away");
let t = nearAPI.providers.getTransactionLastResult(
await userAccount.functionCall({
contractId: config.nftAccount,
methodName: "initiate_transfer",
args: {
asset: ret[0],
token_id: ret[1],
recipient_chain: 8,
recipient: "00112233",
nonce: 5,
},
gas: 300000000000000,
attachedDeposit: 1,
})
);
console.log(t);
console.log(emitter);
console.log("looking it up");
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
emitter,
t,
{
transport: NodeHttpTransport(),
}
);
console.log(signedVAA);
console.log("all done");
process.exit(0);
}
testNearSDK();

450
near/test/p3.ts Normal file
View File

@ -0,0 +1,450 @@
// npx pretty-quick
const sha256 = require("js-sha256");
const nearAPI = require("near-api-js");
const fs = require("fs").promises;
const assert = require("assert").strict;
const fetch = require("node-fetch");
const elliptic = require("elliptic");
const web3Utils = require("web3-utils");
import { zeroPad } from "@ethersproject/bytes";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { Account as nearAccount } from "@certusone/wormhole-sdk/node_modules/near-api-js";
const BN = require("bn.js");
import { TestLib } from "./testlib";
import algosdk, {
Account,
Algodv2,
OnApplicationComplete,
SuggestedParams,
bigIntToBytes,
decodeAddress,
getApplicationAddress,
makeApplicationCallTxnFromObject,
makePaymentTxnWithSuggestedParamsFromObject,
waitForConfirmation,
} from "@certusone/wormhole-sdk/node_modules/algosdk";
import {
createAsset,
getAlgoClient,
getBalance,
getBalances,
getForeignAssetFromVaaAlgorand,
getTempAccounts,
signSendAndConfirmAlgorand,
} from "./algoHelpers";
import {
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
ChainId,
ChainName,
textToHexString,
textToUint8Array,
} from "@certusone/wormhole-sdk/lib/cjs/utils";
import { safeBigIntToNumber } from "@certusone/wormhole-sdk/lib/cjs/utils/bigint";
import {
CONTRACTS,
attestNearFromNear,
attestTokenFromNear,
attestFromAlgorand,
createWrappedOnAlgorand,
createWrappedOnNear,
getEmitterAddressAlgorand,
getForeignAssetAlgorand,
getForeignAssetNear,
getIsTransferCompletedNear,
getIsWrappedAssetNear,
getOriginalAssetNear,
getSignedVAAWithRetry,
redeemOnAlgorand,
redeemOnNear,
transferFromAlgorand,
transferNearFromNear,
transferTokenFromNear,
} from "@certusone/wormhole-sdk";
const wh = require("@certusone/wormhole-sdk");
import { parseSequenceFromLogAlgorand } from "@certusone/wormhole-sdk/lib/cjs/bridge";
import {
getMessageFee,
optin,
TransactionSignerPair,
_parseVAAAlgorand,
} from "@certusone/wormhole-sdk/lib/cjs/algorand";
export const uint8ArrayToHex = (a: Uint8Array): string =>
Buffer.from(a).toString("hex");
export const hexToUint8Array = (h: string): Uint8Array =>
new Uint8Array(Buffer.from(h, "hex"));
function getConfig(env: any) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
wormholeAccount: "wormhole.test.near",
tokenAccount: "token.test.near",
userAccount:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
user2Account:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
};
}
return {};
}
export function logNearGas(result: any, comment: string) {
const { totalGasBurned, totalTokensBurned } = result.receipts_outcome.reduce(
(acc: any, receipt: any) => {
acc.totalGasBurned += receipt.outcome.gas_burnt;
acc.totalTokensBurned += nearAPI.utils.format.formatNearAmount(
receipt.outcome.tokens_burnt
);
return acc;
},
{
totalGasBurned: result.transaction_outcome.outcome.gas_burnt,
totalTokensBurned: nearAPI.utils.format.formatNearAmount(
result.transaction_outcome.outcome.tokens_burnt
),
}
);
console.log(
comment,
"totalGasBurned",
totalGasBurned,
"totalTokensBurned",
totalTokensBurned
);
}
export function parseSequenceFromLogNear(result: any): [number, string] {
let sequence = "";
for (const o of result.receipts_outcome) {
for (const l of o.outcome.logs) {
if (l.startsWith("EVENT_JSON:")) {
const body = JSON.parse(l.slice(11));
if (body.standard == "wormhole" && body.event == "publish") {
return [body.seq, body.emitter];
}
}
}
}
return [-1, ""];
}
async function testNearSDK() {
let config = getConfig(process.env.NEAR_ENV || "sandbox");
// Retrieve the validator key directly in the Tilt environment
const response = await fetch("http://localhost:3031/validator_key.json");
const keyFile = await response.json();
let masterKey = nearAPI.utils.KeyPair.fromString(
keyFile.secret_key || keyFile.private_key
);
let masterPubKey = masterKey.getPublicKey();
let keyStore = new nearAPI.keyStores.InMemoryKeyStore();
keyStore.setKey(config.networkId, config.masterAccount, masterKey);
let near = await nearAPI.connect({
deps: {
keyStore,
},
networkId: config.networkId,
nodeUrl: config.nodeUrl,
});
let masterAccount = new nearAPI.Account(
near.connection,
config.masterAccount
);
console.log(
"Finish init NEAR masterAccount: " +
JSON.stringify(await masterAccount.getAccountBalance())
);
let userKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
keyStore.setKey(config.networkId, config.userAccount, userKey);
let user2Key = nearAPI.utils.KeyPair.fromRandom("ed25519");
keyStore.setKey(config.networkId, config.user2Account, user2Key);
console.log(
"creating a user account: " +
config.userAccount +
" with key " +
userKey.getPublicKey()
);
await masterAccount.createAccount(
config.userAccount,
userKey.getPublicKey(),
new BN(10).pow(new BN(27))
);
const userAccount = new nearAPI.Account(near.connection, config.userAccount);
console.log(
"creating a second user account: " +
config.user2Account +
" with key " +
user2Key.getPublicKey()
);
await masterAccount.createAccount(
config.user2Account,
user2Key.getPublicKey(),
new BN(10).pow(new BN(27))
);
const user2Account = new nearAPI.Account(
near.connection,
config.user2Account
);
let token_bridge = CONTRACTS.DEVNET.near.token_bridge;
let core_bridge = CONTRACTS.DEVNET.near.core;
console.log("Setting up algorand wallet");
let algoCore = BigInt(CONTRACTS.DEVNET.algorand.core);
let algoToken = BigInt(CONTRACTS.DEVNET.algorand.token_bridge);
const tbAddr: string = getApplicationAddress(algoToken);
const decTbAddr: Uint8Array = decodeAddress(tbAddr).publicKey;
const aa: string = uint8ArrayToHex(decTbAddr);
const algoClient: algosdk.Algodv2 = getAlgoClient();
const tempAccts: Account[] = await getTempAccounts();
const numAccts: number = tempAccts.length;
const algoWallet: Account = tempAccts[0];
console.log("Creating USDC on Near");
let ts = new TestLib();
let seq = Math.floor(new Date().getTime() / 1000);
let usdcvaa = ts.hexStringToUint8Array(
ts.genAssetMeta(
ts.singleGuardianPrivKey,
0,
1,
seq,
"4523c3F29447d1f32AEa95BEBD00383c4640F1b4".toLowerCase(),
1,
8,
"USDC",
"CircleCoin"
)
);
seq = seq + 1;
let usdcp = _parseVAAAlgorand(usdcvaa);
let usdc = await createWrappedOnNear(userAccount, token_bridge, usdcvaa);
console.log(usdc);
console.log("Creating USDC token on algorand");
let tx = await createWrappedOnAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
usdcvaa
);
await signSendAndConfirmAlgorand(algoClient, tx, algoWallet);
console.log("Registering the receiving account");
let myAddress = nearAPI.providers.getTransactionLastResult(
await userAccount.functionCall({
contractId: token_bridge,
methodName: "register_account",
args: { account: userAccount.accountId },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
})
);
console.log("myAddress: " + myAddress);
console.log("Airdropping USDC on myself");
{
let trans = ts.genTransfer(
ts.singleGuardianPrivKey,
0,
1,
seq,
10000,
"4523c3F29447d1f32AEa95BEBD00383c4640F1b4".toLowerCase(),
1,
myAddress, // lets send it to the correct user (use the hash)
CHAIN_ID_NEAR,
0
);
console.log(trans);
console.log(
await redeemOnNear(userAccount, token_bridge, hexToUint8Array(trans))
);
}
console.log(".. created some USDC on near");
let wrappedTransfer;
{
console.log("transfer USDC from near to algorand");
let s = await transferTokenFromNear(
userAccount,
core_bridge,
token_bridge,
usdc,
BigInt(100),
decodeAddress(algoWallet.addr).publicKey,
8,
BigInt(0)
);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
s[1],
s[0].toString(),
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
wrappedTransfer = signedVAA;
}
let usdcAssetId;
{
console.log("redeeming our wrapped USDC from Near on Algorand");
const tx = await redeemOnAlgorand(
algoClient,
algoToken,
algoCore,
wrappedTransfer,
algoWallet.addr
);
await signSendAndConfirmAlgorand(algoClient, tx, algoWallet);
let p = _parseVAAAlgorand(wrappedTransfer);
usdcAssetId = (await getForeignAssetAlgorand(
algoClient,
algoToken,
p.FromChain as ChainId,
p.Contract as string
)) as bigint;
console.log("usdc asset id: " + usdcAssetId);
}
const emitterAddr = getEmitterAddressAlgorand(algoToken);
console.log("wallet addr: " + algoWallet.addr);
console.log("usdcAssetId: " + usdcAssetId);
console.log("transfering USDC from Algo To Near... getting the vaa");
console.log("myAddress: " + myAddress);
let testAddress = nearAPI.providers.getTransactionLastResult(
await userAccount.functionCall({
contractId: token_bridge,
methodName: "register_account",
args: { account: "test.test.near" },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
})
);
console.log("testAddress: " + testAddress);
let transferAlgoToNearP3;
{
const AmountToTransfer: number = 100;
const Fee: number = 20;
const transferTxs = await transferFromAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
usdcAssetId,
BigInt(AmountToTransfer),
testAddress,
CHAIN_ID_NEAR,
BigInt(Fee),
hexToUint8Array("ff")
);
const transferResult = await signSendAndConfirmAlgorand(
algoClient,
transferTxs,
algoWallet
);
const txSid = parseSequenceFromLogAlgorand(transferResult);
transferAlgoToNearP3 = (
await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_ALGORAND,
emitterAddr,
txSid,
{ transport: NodeHttpTransport() }
)
).vaaBytes;
}
console.log("redeeming P3 NEAR on Near");
console.log(
await redeemOnNear(user2Account, token_bridge, transferAlgoToNearP3)
);
console.log("transfering USDC from Algo To Near... getting the vaa");
let transferAlgoToNearRandoP3;
{
const AmountToTransfer: number = 100;
const Fee: number = 20;
const transferTxs = await transferFromAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
usdcAssetId,
BigInt(AmountToTransfer),
testAddress,
CHAIN_ID_NEAR,
BigInt(Fee),
hexToUint8Array("ff")
);
const transferResult = await signSendAndConfirmAlgorand(
algoClient,
transferTxs,
algoWallet
);
const txSid = parseSequenceFromLogAlgorand(transferResult);
transferAlgoToNearRandoP3 = (
await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_ALGORAND,
emitterAddr,
txSid,
{ transport: NodeHttpTransport() }
)
).vaaBytes;
}
console.log("What next?");
}
testNearSDK();

134
near/test/recoverTest.ts Normal file
View File

@ -0,0 +1,134 @@
const nearAPI = require("near-api-js");
const BN = require("bn.js");
const fs = require("fs").promises;
const assert = require("assert").strict;
const fetch = require('node-fetch');
const elliptic = require("elliptic");
const web3Utils = require("web3-utils");
function getConfig(env: any) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
contractAccount: Math.floor(Math.random() * 10000).toString() + "wormhole2.test.near",
};
}
}
const contractMethods = {
viewMethods: [],
changeMethods: ["recover_key"],
};
let config :any;
let masterAccount : any;
let masterKey : any;
let pubKey : any;
let keyStore : any;
let near : any;
async function initNear() {
config = getConfig(process.env.NEAR_ENV || "sandbox");
// Retrieve the validator key directly in the Tilt environment
const response = await fetch('http://localhost:3031/validator_key.json');
const keyFile = await response.json();
masterKey = nearAPI.utils.KeyPair.fromString(
keyFile.secret_key || keyFile.private_key
);
pubKey = masterKey.getPublicKey();
keyStore = new nearAPI.keyStores.InMemoryKeyStore();
keyStore.setKey(config.networkId, config.masterAccount, masterKey);
near = await nearAPI.connect({
deps: {
keyStore,
},
networkId: config.networkId,
nodeUrl: config.nodeUrl,
});
masterAccount = new nearAPI.Account(near.connection, config.masterAccount);
console.log("Finish init NEAR: " + JSON.stringify(await masterAccount.getAccountBalance()));
}
async function createContractUser(
accountPrefix : any,
contractAccountId : any,
contractMethod : any
) {
let accountId = Math.floor(Math.random() * 10000).toString() + accountPrefix + "." + config.masterAccount;
console.log(accountId);
let resp = await masterAccount.createAccount(
accountId,
pubKey,
new BN(10).pow(new BN(25))
);
console.log("accountId: " + JSON.stringify(resp))
keyStore.setKey(config.networkId, accountId, masterKey);
const account = new nearAPI.Account(near.connection, accountId);
const accountUseContract = new nearAPI.Contract(
account,
contractAccountId,
contractMethods
);
return accountUseContract;
}
async function initTest() {
const contract = await fs.readFile("./target/wasm32-unknown-unknown/release/near_wormhole.wasm");
const _contractAccount = await masterAccount.createAndDeployContract(
config.contractAccount,
pubKey,
contract,
new BN(10).pow(new BN(26))
);
const wormholeUseContract = await createContractUser(
"wormhole",
config.contractAccount,
contractMethods
);
console.log("Finish deploy contracts and create test accounts");
return { wormholeUseContract };
}
async function test() {
const guardianKey = "beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe";
const guardianPrivKeys = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
const message = Buffer.from("Hello there").toString("hex");
// The actual hash function here doesn't matter that much but this happens to be the hash function used by wormhole
const hash = web3Utils.keccak256(web3Utils.keccak256("0x" + message)).substr(2);
const ec = new elliptic.ec("secp256k1");
const key = ec.keyFromPrivate(guardianPrivKeys);
const signature = key.sign(hash, { canonical: true });
// const key.recoverPubKey(hash, signature, signature.recoveryParam, "hex")
const sig = signature.r.toString(16) + signature.s.toString(16);
console.log("hash: " + hash);
console.log("sig: " + sig);
console.log("originalKey: " + guardianKey);
await initNear();
const { wormholeUseContract } = await initTest();
let ret = await wormholeUseContract.recover_key({ args: { digest: hash, sig: sig, recovery: signature.recoveryParam } });
console.log(ret);
console.log(guardianKey);
}
test();

874
near/test/sdk.ts Normal file
View File

@ -0,0 +1,874 @@
// npx pretty-quick
const sha256 = require("js-sha256");
const fs = require("fs").promises;
const assert = require("assert").strict;
const fetch = require("node-fetch");
const elliptic = require("elliptic");
const web3Utils = require("web3-utils");
import { zeroPad } from "@ethersproject/bytes";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import {
connect as nearConnect,
keyStores as nearKeyStores,
utils as nearUtils,
Account as nearAccount,
providers as nearProviders,
} from "@certusone/wormhole-sdk/node_modules/near-api-js";
const BN = require("bn.js");
import { TestLib } from "./testlib";
import algosdk, {
Account,
Algodv2,
OnApplicationComplete,
SuggestedParams,
bigIntToBytes,
decodeAddress,
getApplicationAddress,
makeApplicationCallTxnFromObject,
makePaymentTxnWithSuggestedParamsFromObject,
waitForConfirmation,
} from "@certusone/wormhole-sdk/node_modules/algosdk";
import {
createAsset,
getAlgoClient,
getBalance,
getBalances,
getForeignAssetFromVaaAlgorand,
getTempAccounts,
signSendAndConfirmAlgorand,
} from "./algoHelpers";
import {
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
ChainId,
ChainName,
textToHexString,
textToUint8Array,
} from "@certusone/wormhole-sdk/lib/cjs/utils";
import { safeBigIntToNumber } from "@certusone/wormhole-sdk/lib/cjs/utils/bigint";
import {
CONTRACTS,
attestNearFromNear,
attestTokenFromNear,
attestFromAlgorand,
createWrappedOnAlgorand,
createWrappedOnNear,
getEmitterAddressAlgorand,
getForeignAssetAlgorand,
getForeignAssetNear,
getIsTransferCompletedNear,
getIsWrappedAssetNear,
getOriginalAssetNear,
getSignedVAAWithRetry,
redeemOnAlgorand,
redeemOnNear,
transferFromAlgorand,
transferNearFromNear,
transferTokenFromNear,
} from "@certusone/wormhole-sdk";
const wh = require("@certusone/wormhole-sdk");
import { parseSequenceFromLogAlgorand } from "@certusone/wormhole-sdk/lib/cjs/bridge";
import {
getMessageFee,
optin,
TransactionSignerPair,
_parseVAAAlgorand,
} from "@certusone/wormhole-sdk/lib/cjs/algorand";
export const uint8ArrayToHex = (a: Uint8Array): string =>
Buffer.from(a).toString("hex");
export const hexToUint8Array = (h: string): Uint8Array =>
new Uint8Array(Buffer.from(h, "hex"));
function getConfig(env: any) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
wormholeAccount: "wormhole.test.near",
tokenAccount: "token.test.near",
userAccount:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
user2Account:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
};
}
return {};
}
export function logNearGas(result: any, comment: string) {
const { totalGasBurned, totalTokensBurned } = result.receipts_outcome.reduce(
(acc: any, receipt: any) => {
acc.totalGasBurned += receipt.outcome.gas_burnt;
acc.totalTokensBurned += nearUtils.format.formatNearAmount(
receipt.outcome.tokens_burnt
);
return acc;
},
{
totalGasBurned: result.transaction_outcome.outcome.gas_burnt,
totalTokensBurned: nearUtils.format.formatNearAmount(
result.transaction_outcome.outcome.tokens_burnt
),
}
);
console.log(
comment,
"totalGasBurned",
totalGasBurned,
"totalTokensBurned",
totalTokensBurned
);
}
export function parseSequenceFromLogNear(result: any): [number, string] {
let sequence = "";
for (const o of result.receipts_outcome) {
for (const l of o.outcome.logs) {
if (l.startsWith("EVENT_JSON:")) {
const body = JSON.parse(l.slice(11));
if (body.standard === "wormhole" && body.event === "publish") {
return [body.seq, body.emitter];
}
}
}
}
return [-1, ""];
}
async function testNearSDK() {
let config = getConfig(process.env.NEAR_ENV || "sandbox");
// Retrieve the validator key directly in the Tilt environment
const response = await fetch("http://localhost:3031/validator_key.json");
const keyFile = await response.json();
let masterKey = nearUtils.KeyPair.fromString(
keyFile.secret_key || keyFile.private_key
);
let masterPubKey = masterKey.getPublicKey();
let keyStore = new nearKeyStores.InMemoryKeyStore();
keyStore.setKey(
config.networkId as string,
config.masterAccount as string,
masterKey
);
let near = await nearConnect({
headers: {},
deps: {
keyStore,
},
networkId: config.networkId as string,
nodeUrl: config.nodeUrl as string,
});
let masterAccount = new nearAccount(
near.connection,
config.masterAccount as string
);
console.log(
"Finish init NEAR masterAccount: " +
JSON.stringify(await masterAccount.getAccountBalance())
);
let userKey = nearUtils.KeyPair.fromRandom("ed25519");
keyStore.setKey(
config.networkId as string,
config.userAccount as string,
userKey
);
let user2Key = nearUtils.KeyPair.fromRandom("ed25519");
keyStore.setKey(
config.networkId as string,
config.user2Account as string,
user2Key
);
console.log(
"creating a user account: " +
config.userAccount +
" with key " +
userKey.getPublicKey()
);
await masterAccount.createAccount(
config.userAccount as string,
userKey.getPublicKey(),
new BN(10).pow(new BN(27))
);
const userAccount = new nearAccount(
near.connection,
config.userAccount as string
);
console.log(
"creating a second user account: " +
config.user2Account +
" with key " +
user2Key.getPublicKey()
);
await masterAccount.createAccount(
config.user2Account as string,
user2Key.getPublicKey(),
new BN(10).pow(new BN(27))
);
const user2Account = new nearAccount(
near.connection,
config.user2Account as string
);
console.log(
"Creating new random non-wormhole token and air dropping some tokens to myself"
);
let randoToken = nearProviders.getTransactionLastResult(
await userAccount.functionCall({
contractId: "test.test.near",
methodName: "deploy_ft",
args: {
account: userAccount.accountId,
},
gas: new BN("300000000000000"),
})
);
let token_bridge = CONTRACTS.DEVNET.near.token_bridge;
let core_bridge = CONTRACTS.DEVNET.near.core;
console.log("Setting up algorand wallet");
let algoCore = BigInt(CONTRACTS.DEVNET.algorand.core);
let algoToken = BigInt(CONTRACTS.DEVNET.algorand.token_bridge);
const tbAddr: string = getApplicationAddress(algoToken);
const decTbAddr: Uint8Array = decodeAddress(tbAddr).publicKey;
const aa: string = uint8ArrayToHex(decTbAddr);
const algoClient: algosdk.Algodv2 = getAlgoClient();
const tempAccts: Account[] = await getTempAccounts();
const numAccts: number = tempAccts.length;
const algoWallet: Account = tempAccts[0];
console.log("Creating USDC on Near");
let ts = new TestLib();
let seq = Math.floor(new Date().getTime() / 1000);
let usdcvaa = ts.hexStringToUint8Array(
ts.genAssetMeta(
ts.singleGuardianPrivKey,
0,
1,
seq,
"4523c3F29447d1f32AEa95BEBD00383c4640F1b4".toLowerCase(),
1,
8,
"USDC",
"CircleCoin"
)
);
seq = seq + 1;
let usdcp = _parseVAAAlgorand(usdcvaa);
//console.log(usdcp);
console.log("calling createWrappedOnNear to create usdc");
if (
(await getIsTransferCompletedNear(userAccount, token_bridge, usdcvaa)) ==
true
) {
console.log("getIsTransferCompleted returned incorrect value (true)");
process.exit(1);
}
let usdc = await createWrappedOnNear(userAccount, token_bridge, usdcvaa);
console.log(usdc);
if (usdc === "") {
console.log("null usdc ... we failed to create it?!");
process.exit(1);
}
if (
(await getIsTransferCompletedNear(userAccount, token_bridge, usdcvaa)) ==
false
) {
console.log("getIsTransferCompleted returned incorrect value (false)");
process.exit(1);
}
let aname = await getForeignAssetNear(
userAccount,
token_bridge,
usdcp.FromChain as ChainId,
usdcp.Contract as string
);
if (aname !== usdc) {
console.log(aname + " !== " + usdc);
process.exit(1);
} else {
console.log(aname + " === " + usdc);
}
console.log("Creating USDC token on algorand");
let tx = await createWrappedOnAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
usdcvaa
);
await signSendAndConfirmAlgorand(algoClient, tx, algoWallet);
let account_hash = await userAccount.viewFunction(
token_bridge,
"hash_account",
{
account: userAccount.accountId,
}
);
console.log(account_hash);
let myAddress = account_hash[1];
console.log("Airdropping USDC on myself");
{
let trans = ts.genTransfer(
ts.singleGuardianPrivKey,
0,
1,
seq,
10000,
"4523c3F29447d1f32AEa95BEBD00383c4640F1b4".toLowerCase(),
1,
myAddress, // lets send it to the correct user (use the hash)
CHAIN_ID_NEAR,
0
);
console.log(trans);
try {
console.log(
await redeemOnNear(userAccount, token_bridge, hexToUint8Array(trans))
);
console.log("This should have thrown a exception..");
process.exit(1);
} catch (error) {
console.log("Exception thrown.. nice.. we dont suck");
console.log(error);
}
console.log("Registering the receiving account");
let myAddress2 = nearProviders.getTransactionLastResult(
await userAccount.functionCall({
contractId: token_bridge,
methodName: "register_account",
args: { account: userAccount.accountId },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
})
);
console.log("myAddress: " + myAddress2);
console.log(
await redeemOnNear(userAccount, token_bridge, hexToUint8Array(trans))
);
}
console.log(".. created some USDC");
let nativeAttest;
{
console.log("attesting: " + randoToken);
let s = await attestTokenFromNear(
userAccount,
core_bridge,
token_bridge,
randoToken
);
console.log(s);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
s[1],
s[0].toString(),
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
let p = _parseVAAAlgorand(signedVAA);
console.log(p.FromChain as ChainId, p.Contract as string);
let a = await getForeignAssetNear(
userAccount,
token_bridge,
p.FromChain as ChainId,
p.Contract as string
);
if (a !== randoToken) {
console.log(a + " !== " + randoToken);
process.exit(1);
}
nativeAttest = signedVAA;
}
let nearAttest;
{
console.log("attesting Near itself");
let s = await attestNearFromNear(userAccount, core_bridge, token_bridge);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
s[1],
s[0].toString(),
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
let p = _parseVAAAlgorand(signedVAA);
let a = await getForeignAssetNear(
userAccount,
token_bridge,
p.FromChain as ChainId,
p.Contract as string
);
console.log(
"chain: {} contract: {} account: {}",
p.FromChain,
p.Contract,
a
);
nearAttest = signedVAA;
}
console.log("Creating a native token from near onto algorand");
tx = await createWrappedOnAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
nativeAttest
);
await signSendAndConfirmAlgorand(algoClient, tx, algoWallet);
console.log("Creating NEAR from near onto algorand");
tx = await createWrappedOnAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
nearAttest
);
await signSendAndConfirmAlgorand(algoClient, tx, algoWallet);
console.log("Shock and awe...");
if (usdc === "") {
console.log("null usdc");
process.exit(1);
}
let wrappedTransfer;
{
console.log(
"transfer wrapped token from near to algorand",
userAccount,
core_bridge,
token_bridge,
usdc
);
let s = await transferTokenFromNear(
userAccount,
core_bridge,
token_bridge,
usdc,
BigInt(100),
decodeAddress(algoWallet.addr).publicKey,
8,
BigInt(0)
);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
s[1],
s[0].toString(),
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
wrappedTransfer = signedVAA;
}
let randoTransfer;
{
console.log("transfer rando token from near to algorand");
let s = await transferTokenFromNear(
userAccount,
core_bridge,
token_bridge,
randoToken,
BigInt(10000000),
decodeAddress(algoWallet.addr).publicKey,
8,
BigInt(0)
);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
s[1],
s[0].toString(),
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
randoTransfer = signedVAA;
}
let nearTransfer;
{
console.log("transfer near from near to algorand");
let s = await transferNearFromNear(
userAccount,
core_bridge,
token_bridge,
BigInt(1000000000000000000000000),
decodeAddress(algoWallet.addr).publicKey,
8,
BigInt(0)
);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
s[1],
s[0].toString(),
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
nearTransfer = signedVAA;
}
let usdcAssetId;
{
console.log("redeeming our wrapped USDC from Near on Algorand");
const tx = await redeemOnAlgorand(
algoClient,
algoToken,
algoCore,
wrappedTransfer,
algoWallet.addr
);
await signSendAndConfirmAlgorand(algoClient, tx, algoWallet);
let p = _parseVAAAlgorand(wrappedTransfer);
usdcAssetId = (await getForeignAssetAlgorand(
algoClient,
algoToken,
p.FromChain as ChainId,
p.Contract as string
)) as bigint;
console.log("usdc asset id: " + usdcAssetId);
}
let randoAssetId;
{
console.log("redeeming our near native asset on Algorand");
const tx = await redeemOnAlgorand(
algoClient,
algoToken,
algoCore,
randoTransfer,
algoWallet.addr
);
await signSendAndConfirmAlgorand(algoClient, tx, algoWallet);
let p = _parseVAAAlgorand(randoTransfer);
randoAssetId = (await getForeignAssetAlgorand(
algoClient,
algoToken,
p.FromChain as ChainId,
p.Contract as string
)) as bigint;
console.log("randoToken asset id: " + randoAssetId);
}
let nearAssetId;
{
console.log("redeeming NEAR on Algorand");
const tx = await redeemOnAlgorand(
algoClient,
algoToken,
algoCore,
nearTransfer,
algoWallet.addr
);
await signSendAndConfirmAlgorand(algoClient, tx, algoWallet);
let p = _parseVAAAlgorand(nearTransfer);
nearAssetId = (await getForeignAssetAlgorand(
algoClient,
algoToken,
p.FromChain as ChainId,
p.Contract as string
)) as bigint;
console.log("NEAR asset id: " + nearAssetId);
}
const emitterAddr = getEmitterAddressAlgorand(algoToken);
console.log("wallet addr: " + algoWallet.addr);
console.log("usdcAssetId: " + usdcAssetId);
console.log("transfering USDC from Algo To Near... getting the vaa");
console.log("myAddress: " + myAddress);
let transferAlgoToNearUSDC;
{
const AmountToTransfer: number = 100;
const Fee: number = 20;
const transferTxs = await transferFromAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
usdcAssetId,
BigInt(AmountToTransfer),
myAddress,
CHAIN_ID_NEAR,
BigInt(Fee)
);
const transferResult = await signSendAndConfirmAlgorand(
algoClient,
transferTxs,
algoWallet
);
const txSid = parseSequenceFromLogAlgorand(transferResult);
transferAlgoToNearUSDC = (
await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_ALGORAND,
emitterAddr,
txSid,
{ transport: NodeHttpTransport() }
)
).vaaBytes;
}
console.log("transfering rando from Algo To Near... getting the vaa");
let transferAlgoToNearRando;
{
const AmountToTransfer: number = 100;
const Fee: number = 20;
const transferTxs = await transferFromAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
randoAssetId,
BigInt(AmountToTransfer),
myAddress,
CHAIN_ID_NEAR,
BigInt(Fee)
);
const transferResult = await signSendAndConfirmAlgorand(
algoClient,
transferTxs,
algoWallet
);
const txSid = parseSequenceFromLogAlgorand(transferResult);
transferAlgoToNearRando = (
await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_ALGORAND,
emitterAddr,
txSid,
{ transport: NodeHttpTransport() }
)
).vaaBytes;
}
console.log("transfering NEAR from Algo To Near... getting the vaa");
let transferAlgoToNearNEAR;
{
const AmountToTransfer: number = 100;
const Fee: number = 20;
const transferTxs = await transferFromAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
nearAssetId,
BigInt(AmountToTransfer),
myAddress,
CHAIN_ID_NEAR,
BigInt(Fee)
);
const transferResult = await signSendAndConfirmAlgorand(
algoClient,
transferTxs,
algoWallet
);
const txSid = parseSequenceFromLogAlgorand(transferResult);
transferAlgoToNearNEAR = (
await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_ALGORAND,
emitterAddr,
txSid,
{ transport: NodeHttpTransport() }
)
).vaaBytes;
}
console.log("redeeming USDC on Near");
console.log(
await redeemOnNear(user2Account, token_bridge, transferAlgoToNearUSDC)
);
console.log(
"redeeming Rando on Near: " + uint8ArrayToHex(transferAlgoToNearRando)
);
console.log(
await redeemOnNear(user2Account, token_bridge, transferAlgoToNearRando)
);
console.log("redeeming NEAR on Near");
console.log(
await redeemOnNear(user2Account, token_bridge, transferAlgoToNearNEAR)
);
let userAccount2Address = nearProviders.getTransactionLastResult(
await userAccount.functionCall({
contractId: token_bridge,
methodName: "register_account",
args: { account: user2Account.accountId },
gas: new BN("100000000000000"),
attachedDeposit: new BN("2000000000000000000000"), // 0.002 NEAR
})
);
console.log("userAccount2Address: " + userAccount2Address);
let transferAlgoToNearP3;
{
const AmountToTransfer: number = 100;
const Fee: number = 20;
const transferTxs = await transferFromAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
nearAssetId,
BigInt(AmountToTransfer),
userAccount2Address,
CHAIN_ID_NEAR,
BigInt(Fee),
hexToUint8Array("ff")
);
const transferResult = await signSendAndConfirmAlgorand(
algoClient,
transferTxs,
algoWallet
);
const txSid = parseSequenceFromLogAlgorand(transferResult);
transferAlgoToNearP3 = (
await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_ALGORAND,
emitterAddr,
txSid,
{ transport: NodeHttpTransport() }
)
).vaaBytes;
}
{
console.log("redeeming P3 NEAR on Near");
console.log(
await redeemOnNear(user2Account, token_bridge, transferAlgoToNearP3)
);
console.log("transfering rando from Algo To Near... getting the vaa");
let transferAlgoToNearRandoP3;
{
const AmountToTransfer: number = 100;
const Fee: number = 20;
const transferTxs = await transferFromAlgorand(
algoClient,
algoToken,
algoCore,
algoWallet.addr,
randoAssetId,
BigInt(AmountToTransfer),
userAccount2Address,
CHAIN_ID_NEAR,
BigInt(Fee),
hexToUint8Array("ff")
);
const transferResult = await signSendAndConfirmAlgorand(
algoClient,
transferTxs,
algoWallet
);
const txSid = parseSequenceFromLogAlgorand(transferResult);
transferAlgoToNearRandoP3 = (
await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_ALGORAND,
emitterAddr,
txSid,
{ transport: NodeHttpTransport() }
)
).vaaBytes;
}
console.log("redeeming P3 random on Near");
console.log(
await redeemOnNear(user2Account, token_bridge, transferAlgoToNearRandoP3)
);
}
console.log("What next?");
}
testNearSDK();

900
near/test/test.ts Normal file
View File

@ -0,0 +1,900 @@
// npx pretty-quick
const sha256 = require("js-sha256");
const nearAPI = require("near-api-js");
const BN = require("bn.js");
const fs = require("fs").promises;
const assert = require("assert").strict;
const fetch = require("node-fetch");
const elliptic = require("elliptic");
const web3Utils = require("web3-utils");
import { zeroPad } from "@ethersproject/bytes";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { TestLib } from "./testlib";
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
} from "@certusone/wormhole-sdk/lib/cjs/utils";
import { _parseVAAAlgorand } from "@certusone/wormhole-sdk/lib/cjs/algorand";
import { getSignedVAAWithRetry } from "@certusone/wormhole-sdk";
function getConfig(env: any) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
wormholeAccount:
Math.floor(Math.random() * 10000).toString() + "wormhole.test.near",
tokenAccount:
Math.floor(Math.random() * 10000).toString() + "token.test.near",
testAccount:
Math.floor(Math.random() * 10000).toString() + "test.test.near",
userAccount:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
};
}
}
const wormholeMethods = {
viewMethods: [],
changeMethods: ["boot_wormhole", "submit_vaa"],
};
const tokenMethods = {
viewMethods: [],
changeMethods: [
"boot_portal",
"submit_vaa",
"submit_vaa_callback",
"attest_near",
"attest_token",
"send_transfer_near",
"send_transfer_wormhole_token",
"account_hash",
],
};
const testMethods = {
viewMethods: [],
changeMethods: ["deploy_ft"],
};
const ftMethods = {
viewMethods: [],
changeMethods: ["ft_transfer_call", "storage_deposit"],
};
let config: any;
let masterAccount: any;
let _tokenAccount: any;
let _wormholeAccount: any;
let _testAccount: any;
let masterKey: any;
let masterPubKey: any;
let keyStore: any;
let near: any;
let userAccount: any;
let userKey: any;
let userPubKey: any;
async function initNear() {
config = getConfig(process.env.NEAR_ENV || "sandbox");
// Retrieve the validator key directly in the Tilt environment
const response = await fetch("http://localhost:3031/validator_key.json");
const keyFile = await response.json();
console.log(keyFile);
masterKey = nearAPI.utils.KeyPair.fromString(
keyFile.secret_key || keyFile.private_key
);
masterPubKey = masterKey.getPublicKey();
userKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
console.log(userKey);
keyStore = new nearAPI.keyStores.InMemoryKeyStore();
keyStore.setKey(config.networkId, config.masterAccount, masterKey);
keyStore.setKey(config.networkId, config.userAccount, userKey);
near = await nearAPI.connect({
deps: {
keyStore,
},
networkId: config.networkId,
nodeUrl: config.nodeUrl,
});
masterAccount = new nearAPI.Account(near.connection, config.masterAccount);
console.log(
"Finish init NEAR masterAccount: " +
JSON.stringify(await masterAccount.getAccountBalance())
);
let resp = await masterAccount.createAccount(
config.userAccount,
userKey.getPublicKey(),
new BN(10).pow(new BN(25))
);
console.log(resp);
userAccount = new nearAPI.Account(near.connection, config.userAccount);
console.log(
"Finish init NEAR userAccount: " +
JSON.stringify(await userAccount.getAccountBalance())
);
// console.log(await userAccount.sendMoney(config.masterAccount, nearAPI.utils.format.parseNearAmount("1.5")));;
// console.log("Sent some money: " + JSON.stringify(await userAccount.getAccountBalance()));
}
async function createContractUser(
accountPrefix: any,
contractAccountId: any,
methods: any
) {
let accountId =
Math.floor(Math.random() * 10000).toString() +
accountPrefix +
"." +
config.masterAccount;
console.log(accountId);
let randomKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
let resp = await masterAccount.createAccount(
accountId,
randomKey.getPublicKey(),
new BN(10).pow(new BN(28))
);
console.log("accountId: " + JSON.stringify(resp));
keyStore.setKey(config.networkId, accountId, randomKey);
const account = new nearAPI.Account(near.connection, accountId);
const accountUseContract = new nearAPI.Contract(
account,
contractAccountId,
methods
);
return accountUseContract;
}
async function initTest() {
const wormholeContract = await fs.readFile(
"./contracts/wormhole/target/wasm32-unknown-unknown/release/near_wormhole.wasm"
);
const tokenContract = await fs.readFile(
"./contracts/portal/target/wasm32-unknown-unknown/release/near_token_bridge.wasm"
);
const testContract = await fs.readFile(
"./contracts/mock-bridge-integration/target/wasm32-unknown-unknown/release/near_mock_bridge_integration.wasm"
);
let randomKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
keyStore.setKey(config.networkId, config.wormholeAccount, randomKey);
_wormholeAccount = await masterAccount.createAndDeployContract(
config.wormholeAccount,
randomKey.getPublicKey(),
wormholeContract,
new BN(10).pow(new BN(27))
);
randomKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
keyStore.setKey(config.networkId, config.tokenAccount, randomKey);
_tokenAccount = await masterAccount.createAndDeployContract(
config.tokenAccount,
randomKey.getPublicKey(),
tokenContract,
new BN(10).pow(new BN(27))
);
console.log("tokenAccount: " + config.tokenAccount);
_testAccount = await masterAccount.createAndDeployContract(
config.testAccount,
randomKey.getPublicKey(),
testContract,
new BN(10).pow(new BN(27))
);
const wormholeUseContract = await createContractUser(
"wormhole_user",
config.wormholeAccount,
wormholeMethods
);
const tokenUseContract = await createContractUser(
"tokenbridge_user",
config.tokenAccount,
tokenMethods
);
const testUseContract = await createContractUser(
"test_user",
config.testAccount,
testMethods
);
//
// console.log(userUseContract.account.accountId);
console.log("Finish deploy contracts and create test accounts");
return {
wormholeUseContract,
tokenUseContract,
testUseContract,
};
}
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function nearParseResultForLogs(result: any): [number, string] {
for (const o of result.receipts_outcome) {
for (const l of o.outcome.logs) {
console.log(l);
if (l.startsWith("EVENT_JSON:")) {
const body = JSON.parse(l.slice(11));
if (body.standard == "wormhole" && body.event == "publish") {
console.log(body);
return [body.seq, body.emitter];
}
}
}
}
return [-1, ""];
}
async function test() {
let fastTest = true;
let ts = new TestLib();
await initNear();
const { wormholeUseContract, tokenUseContract, testUseContract } =
await initTest();
console.log("Booting guardian set with index 0");
await wormholeUseContract.boot_wormhole({
args: { gset: 0, addresses: ts.guardianKeys },
});
console.log("Completed without an error... odd.. I am not sucking yet");
console.log("Booting up the token bridge");
await tokenUseContract.boot_portal({
args: { core: config.wormholeAccount },
});
console.log("token bridge booted");
let seq = 1;
console.log("lets upgrade the governance set to 1");
let vaa = ts.genGuardianSetUpgrade(
ts.guardianPrivKeys,
0,
1,
1,
seq,
ts.guardianKeys
);
console.log("sending it to the core contract");
await wormholeUseContract.submit_vaa({ args: { vaa: vaa } });
seq = seq + 1;
if (!fastTest) {
console.log("Its parsed... lets do it again!!");
try {
await wormholeUseContract.submit_vaa({ args: { vaa: vaa } });
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. nice.. we dont suck");
}
console.log(
"Lets try to send a governence message (SetFee) with the wrong index"
);
vaa = ts.genGSetFee(ts.guardianPrivKeys, 0, 1, seq, CHAIN_ID_NEAR, 5);
try {
await wormholeUseContract.submit_vaa({ args: { vaa: vaa } });
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log(
"Exception thrown.. nice.. this was with the wrong governance set"
);
}
console.log(
"Lets try to send a governence message (SetFee) with the correct index but the wrong chain"
);
vaa = ts.genGSetFee(ts.guardianPrivKeys, 1, 1, seq, CHAIN_ID_ALGORAND, 5);
try {
await wormholeUseContract.submit_vaa({ args: { vaa: vaa } });
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. that is correct... ");
}
console.log(
"Lets try to send a governence message (SetFee) with the correct index but for all chains"
);
vaa = ts.genGSetFee(ts.guardianPrivKeys, 1, 1, seq, 0, 5);
try {
await wormholeUseContract.submit_vaa({ args: { vaa: vaa } });
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. that is correct... ");
}
console.log(
"Lets try to send a governence message (SetFee) with the correct index and the correct chain"
);
vaa = ts.genGSetFee(ts.guardianPrivKeys, 1, 1, seq, CHAIN_ID_NEAR, 5);
await wormholeUseContract.submit_vaa({ args: { vaa: vaa } });
console.log("boo yaah! this was supposed to pass and it did");
seq = seq + 1;
console.log("lets try to call the vaa_vallback directly");
try {
await tokenUseContract.submit_vaa_callback({ args: {} });
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. that is correct... ");
}
try {
vaa = ts.genRegisterChain(ts.guardianPrivKeys, 0, 1, seq, 1);
console.log(
"Now lets call submit_vaa with a valid vaa (register the solana chain) on the token bridge.. with the wrong governance set"
);
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
});
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. that is correct... ");
}
}
vaa = ts.genRegisterChain(ts.guardianPrivKeys, 1, 1, seq, 1);
let is_completed = await tokenUseContract.account.viewFunction(config.tokenAccount, "is_transfer_completed", { vaa: vaa });
console.log("is_transfer_completed: %s", is_completed);
console.log(
"Now lets call submit_vaa with a valid vaa (register the solana chain) on the token bridge.. with the correct governance set"
);
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
});
seq = seq + 1;
is_completed = await tokenUseContract.account.viewFunction(config.tokenAccount, "is_transfer_completed", { vaa: vaa });
console.log("is_transfer_completed: %s", is_completed);
if (!fastTest) {
try {
vaa = ts.genRegisterChain(ts.guardianPrivKeys, 1, 1, seq, 1);
console.log(
"Now lets call submit_vaa with a valid vaa (register the solana chain) again.. again... this should fail"
);
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
});
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. that is correct... ");
}
}
let usdcEmitter = "4523c3F29447d1f32AEa95BEBD00383c4640F1b4".toLowerCase();
if (!fastTest) {
try {
vaa =
ts.genAssetMeta(
ts.guardianPrivKeys,
1,
1,
seq,
usdcEmitter,
1,
8,
"USDC",
"CircleCoin"
) + "00";
console.log(
"Now the fun stuff... lets create some USDC... but pass a hacked vaa"
);
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
});
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. that is correct... ");
}
}
vaa = ts.genAssetMeta(
ts.guardianPrivKeys,
1,
1,
seq,
usdcEmitter,
1,
8,
"USDC2",
"CircleCoin2"
);
console.log("Now the fun stuff... lets create some USDC");
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
});
console.log("Try again (since this is an attest)");
let tname = await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
amount: "33000000000000000000000000",
});
console.log("tname: " + tname);
console.log("get_original_asset: " + await tokenUseContract.account.viewFunction(config.tokenAccount, "get_original_asset", { token: tname }));
seq = seq + 1;
if (!fastTest) {
console.log("Lets attest the same thing again");
vaa = ts.genAssetMeta(
ts.guardianPrivKeys,
1,
1,
seq,
usdcEmitter,
1,
8,
"USDC",
"CircleCoin"
);
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
});
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
amount: "20000000000000000000000000",
});
seq = seq + 1;
try {
console.log("Lets make it fail");
vaa = ts.genAssetMeta(
ts.guardianPrivKeys,
1,
1,
seq,
usdcEmitter,
1,
8,
"USDC20",
"OnceUponATimeFarAway"
);
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
});
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
});
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. that is correct... ");
}
seq = seq + 1;
}
console.log(
"Now, for something useful? send some USDC to our test account: " +
tokenUseContract.account.accountId
);
vaa = ts.genTransfer(
ts.guardianPrivKeys,
1,
1,
seq,
100000,
usdcEmitter,
1,
Buffer.from(tokenUseContract.account.accountId).toString("hex"),
CHAIN_ID_NEAR,
0
);
console.log(vaa);
// console.log(_parseVAAAlgorand(ts.hexStringToUint8Array(vaa)));
await tokenUseContract.submit_vaa({
args: { vaa: vaa },
gas: 300000000000000,
});
console.log("well? did it work?!");
console.log("npm i -g near-cli");
console.log(
"near --nodeUrl http://localhost:3030 view " +
tname +
' ft_balance_of \'{"account_id": "' +
tokenUseContract.account.accountId +
"\"}'"
);
seq = seq + 1;
if (!fastTest) {
try {
console.log(
"attesting near.. but not attaching any cash to cover this as the first time this emitter is being seen"
);
let sequence = await tokenUseContract.attest_near({
args: {},
gas: 100000000000000,
});
console.log(sequence);
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. that is correct... ");
}
}
let ah = await tokenUseContract.account_hash({ args: {} });
console.log(ah);
let emitter = sha256.sha256.hex(ah[0]);
if (ah[0] != config.tokenAccount) {
console.log("The token account does not match what I think it should be");
process.exit(1);
}
if (ah[1] != emitter) {
console.log(
"The sha256 hash of the token account does not match what I think it should be"
);
process.exit(1);
}
console.log("emitter: " + emitter);
if (!fastTest) {
{
console.log("attesting near but paying for it ");
let sequence = await tokenUseContract.attest_near({
args: {},
gas: 100000000000000,
amount: "790000000000000000000",
});
console.log(sequence);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
emitter,
sequence,
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
}
{
console.log("attesting wormhole token: " + tname);
let sequence = await tokenUseContract.attest_token({
args: { token: tname },
gas: 100000000000000,
});
console.log(sequence);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
emitter,
sequence,
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
}
}
console.log("deploying a random token and sending some money to a user");
let randoToken = await testUseContract.deploy_ft({
args: { account: tokenUseContract.account.accountId },
amount: "7900000000000001234",
gas: 300000000000000,
});
console.log(
"near --nodeUrl http://localhost:3030 view " +
randoToken +
' ft_balance_of \'{"account_id": "' +
tokenUseContract.account.accountId +
"\"}'"
);
if (!fastTest) {
{
console.log("attesting non wormhole token: " + randoToken);
let sequence = await tokenUseContract.attest_token({
args: { token: randoToken },
gas: 100000000000000,
});
console.log(sequence);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
emitter,
sequence,
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
console.log(_parseVAAAlgorand(signedVAA));
}
{
console.log("sending some NEAR off to a HODLr in the cloud");
let sequence = await tokenUseContract.send_transfer_near({
args: { receiver: "0011223344", chain: 1, fee: 0, payload: "" },
amount: "7900000000000001234",
gas: 100000000000000,
});
console.log(sequence);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
emitter,
sequence,
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
console.log(_parseVAAAlgorand(signedVAA));
}
try {
console.log("sending some RANDO off to a HODLr in the cloud");
await tokenUseContract.send_transfer_wormhole_token({
args: {
token: randoToken,
receiver: "0011223344",
chain: 1,
fee: 0,
payload: "",
amount: 12345,
},
amount: "7900000000000001234",
gas: 100000000000000,
});
console.log("This should have thrown a exception..");
process.exit(1);
} catch {
console.log("Exception thrown.. nice.. we dont suck");
}
{
console.log("sending some USDC off to a HODLr in the cloud: " + tname);
console.log(ah);
let sequence = await tokenUseContract.send_transfer_wormhole_token({
args: {
amount: 12345,
token: tname,
receiver: "0011223344",
chain: 1,
fee: 0,
payload: "",
},
amount: "800000000000000000000",
gas: 100000000000000,
});
console.log(sequence);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
emitter,
sequence,
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
console.log(_parseVAAAlgorand(signedVAA));
}
}
const tokenContract = new nearAPI.Contract(
tokenUseContract.account,
randoToken,
ftMethods
);
console.log(
"tokenUseContract.account.accountId",
tokenUseContract.account.accountId
);
console.log(
"tokenContract.account.accountId",
tokenContract.account.accountId
);
console.log(
"testUseContract.account.accountId",
testUseContract.account.accountId
);
console.log("tokenUseContract.contractId", tokenUseContract.contractId);
console.log("config.tokenAccount", config.tokenAccount);
console.log("paying for storage for the destination account");
console.log(
await tokenContract.storage_deposit({
args: {
account_id: config.tokenAccount,
registation_only: true,
},
amount: "12500000000000000000000",
gas: 100000000000000,
})
);
{
let result = await tokenUseContract.account.functionCall({
contractId: randoToken,
methodName: "ft_transfer_call",
args: {
receiver_id: config.tokenAccount,
amount: "3210000000",
msg: JSON.stringify(
{
receiver: "33445566",
chain: 1,
fee: 0,
payload: "",
}
)
},
attachedDeposit: "1",
gas: 100000000000000,
});
let s = nearParseResultForLogs(result);
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
["http://localhost:7071"],
CHAIN_ID_NEAR,
s[1],
s[0].toString(),
{
transport: NodeHttpTransport(),
}
);
console.log("vaa: " + Buffer.from(signedVAA).toString("hex"));
console.log(_parseVAAAlgorand(signedVAA));
}
// console.log(result);
// const flatLogs = [
// result.transaction_outcome,
// ...result.receipts_outcome,
// ].reduce((acc, it) => {
// if (
// it.outcome.logs.length ||
// (typeof it.outcome.status === "object" &&
// typeof it.outcome.status.Failure === "object")
// ) {
// return acc.concat({
// receiptIds: it.outcome.receipt_ids,
// logs: it.outcome.logs,
// failure:
// typeof it.outcome.status.Failure != "undefined"
// ? it.outcome.status.Failure
// : null,
// });
// } else return acc;
// }, []);
//
// console.log(flatLogs);
//
// const { totalGasBurned, totalTokensBurned } = result.receipts_outcome.reduce(
// (acc : any, receipt : any) => {
// acc.totalGasBurned += receipt.outcome.gas_burnt;
// acc.totalTokensBurned += nearAPI.utils.format.formatNearAmount(
// receipt.outcome.tokens_burnt
// );
// return acc;
// },
// {
// totalGasBurned: result.transaction_outcome.outcome.gas_burnt,
// totalTokensBurned: nearAPI.utils.format.formatNearAmount(
// result.transaction_outcome.outcome.tokens_burnt
// ),
// }
// );
//
// console.log(
// "totalGasBurned",
// totalGasBurned,
// "totalTokensBurned",
// totalTokensBurned
// );
//
// console.log("result: ", nearAPI.providers.getTransactionLastResult(result));
//
// console.log(JSON.stringify(result, null, 2));
//
// {
// let out = []
// for (const idx in result.receipts_outcome) {
// let r = result.receipts_outcome[idx];
// out.push({
// executor: r.outcome.executor_id,
// gas: r.outcome.gas_burnt,
// token: r.outcome.tokens_burnt,
// logs: r.outcome.logs
// });
// }
// console.log(JSON.stringify(out, null, 2));
// }
console.log("test complete");
}
test();

284
near/test/test2.ts Normal file
View File

@ -0,0 +1,284 @@
// npx pretty-quick
const sha256 = require("js-sha256");
const nearAPI = require("near-api-js");
const BN = require("bn.js");
const fs = require("fs").promises;
const assert = require("assert").strict;
const fetch = require("node-fetch");
const elliptic = require("elliptic");
const web3Utils = require("web3-utils");
import { zeroPad } from "@ethersproject/bytes";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { TestLib } from "./testlib";
import {
ChainId,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
} from "@certusone/wormhole-sdk/lib/cjs/utils";
import { _parseVAAAlgorand } from "@certusone/wormhole-sdk/lib/cjs/algorand";
import { getSignedVAAWithRetry } from "@certusone/wormhole-sdk";
function getConfig(env: any) {
switch (env) {
case "sandbox":
case "local":
return {
networkId: "sandbox",
nodeUrl: "http://localhost:3030",
masterAccount: "test.near",
wormholeAccount:
Math.floor(Math.random() * 10000).toString() + "wormhole.test.near",
tokenAccount:
Math.floor(Math.random() * 10000).toString() + "token.test.near",
testAccount:
Math.floor(Math.random() * 10000).toString() + "test.test.near",
userAccount:
Math.floor(Math.random() * 10000).toString() + "user.test.near",
};
}
}
const wormholeMethods = {
viewMethods: [],
changeMethods: ["boot_wormhole", "submit_vaa"],
};
const tokenMethods = {
viewMethods: [],
changeMethods: [
"boot_portal",
"submit_vaa",
"submit_vaa_callback",
"attest_near",
"attest_token",
"send_transfer_near",
"send_transfer_wormhole_token",
"account_hash",
],
};
const testMethods = {
viewMethods: [],
changeMethods: ["deploy_ft"],
};
const ftMethods = {
viewMethods: [],
changeMethods: ["ft_transfer_call", "storage_deposit"],
};
let config: any;
let masterAccount: any;
let _tokenAccount: any;
let _wormholeAccount: any;
let _testAccount: any;
let masterKey: any;
let masterPubKey: any;
let keyStore: any;
let near: any;
let userAccount: any;
let userKey: any;
let userPubKey: any;
async function initNear() {
config = getConfig(process.env.NEAR_ENV || "sandbox");
// Retrieve the validator key directly in the Tilt environment
const response = await fetch("http://localhost:3031/validator_key.json");
const keyFile = await response.json();
console.log(keyFile);
masterKey = nearAPI.utils.KeyPair.fromString(
keyFile.secret_key || keyFile.private_key
);
masterPubKey = masterKey.getPublicKey();
userKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
console.log(userKey);
keyStore = new nearAPI.keyStores.InMemoryKeyStore();
keyStore.setKey(config.networkId, config.masterAccount, masterKey);
keyStore.setKey(config.networkId, config.userAccount, userKey);
near = await nearAPI.connect({
deps: {
keyStore,
},
networkId: config.networkId,
nodeUrl: config.nodeUrl,
});
masterAccount = new nearAPI.Account(near.connection, config.masterAccount);
console.log(
"Finish init NEAR masterAccount: " +
JSON.stringify(await masterAccount.getAccountBalance())
);
let resp = await masterAccount.createAccount(
config.userAccount,
userKey.getPublicKey(),
new BN(10).pow(new BN(25))
);
console.log(resp);
userAccount = new nearAPI.Account(near.connection, config.userAccount);
console.log(
"Finish init NEAR userAccount: " +
JSON.stringify(await userAccount.getAccountBalance())
);
// console.log(await userAccount.sendMoney(config.masterAccount, nearAPI.utils.format.parseNearAmount("1.5")));;
// console.log("Sent some money: " + JSON.stringify(await userAccount.getAccountBalance()));
}
async function createContractUser(
accountPrefix: any,
contractAccountId: any,
methods: any
) {
let accountId =
Math.floor(Math.random() * 10000).toString() +
accountPrefix +
"." +
config.masterAccount;
console.log(accountId);
let randomKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
let resp = await masterAccount.createAccount(
accountId,
randomKey.getPublicKey(),
new BN(10).pow(new BN(28))
);
console.log("accountId: " + JSON.stringify(resp));
keyStore.setKey(config.networkId, accountId, randomKey);
const account = new nearAPI.Account(near.connection, accountId);
const accountUseContract = new nearAPI.Contract(
account,
contractAccountId,
methods
);
return accountUseContract;
}
async function initTest() {
const wormholeContract = await fs.readFile(
"./contracts/wormhole/target/wasm32-unknown-unknown/release/near_wormhole.wasm"
);
const tokenContract = await fs.readFile(
"./contracts/portal/target/wasm32-unknown-unknown/release/near_token_bridge.wasm"
);
const testContract = await fs.readFile(
"./contracts/mock-bridge-integration/target/wasm32-unknown-unknown/release/near_mock_bridge_integration.wasm"
);
let randomKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
keyStore.setKey(config.networkId, config.wormholeAccount, randomKey);
_wormholeAccount = await masterAccount.createAndDeployContract(
config.wormholeAccount,
randomKey.getPublicKey(),
wormholeContract,
new BN(10).pow(new BN(27))
);
randomKey = nearAPI.utils.KeyPair.fromRandom("ed25519");
keyStore.setKey(config.networkId, config.tokenAccount, randomKey);
_tokenAccount = await masterAccount.createAndDeployContract(
config.tokenAccount,
randomKey.getPublicKey(),
tokenContract,
new BN(10).pow(new BN(27))
);
console.log("tokenAccount: " + config.tokenAccount);
_testAccount = await masterAccount.createAndDeployContract(
config.testAccount,
randomKey.getPublicKey(),
testContract,
new BN(10).pow(new BN(27))
);
const wormholeUseContract = await createContractUser(
"wormhole_user",
config.wormholeAccount,
wormholeMethods
);
console.log("Finish deploy contracts and create test accounts");
return {
wormholeUseContract,
};
}
function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function nearParseResultForLogs(result: any): [number, string] {
for (const o of result.receipts_outcome) {
for (const l of o.outcome.logs) {
console.log(l);
if (l.startsWith("EVENT_JSON:")) {
const body = JSON.parse(l.slice(11));
if (body.standard == "wormhole" && body.event == "publish") {
console.log(body);
return [body.seq, body.emitter];
}
}
}
}
return [-1, ""];
}
async function test() {
let fastTest = true;
let ts = new TestLib();
let vaa =
"01000000000100eea5fa9ff5e88efeec83febdb6281a23152fe176d76e0f625781c93636e73ec309530f0fdebb24f410f255215d3b0f9fcb2cfafce3f9a8b97ac6640b32d24c8401000000010000000100010000000000000000000000000000000000000000000000000000000000000004000000000315346a00000000000000000000000000000000000000000000546f6b656e4272696467650100000001c69a1b1a65dd336bf1df6a77afb501fc25db7fc0938cb08595a9ef473265cb4f";
let p = _parseVAAAlgorand(new Uint8Array(Buffer.from(vaa, "hex")));
console.log(ts.uint8ArrayToHexString(p.targetEmitter as Uint8Array, false));
await initNear();
const { wormholeUseContract } = await initTest();
console.log("Booting guardian set with index 0");
console.log(ts.singleGuardianKey);
await wormholeUseContract.boot_wormhole({
args: { gset: 0, addresses: ts.singleGuardianKey },
});
console.log("Completed without an error... odd.. I am not sucking yet");
let result = await userAccount.functionCall({
contractId: config.wormholeAccount,
methodName: "verify_vaa",
args: {
vaa,
},
gas: 100000000000000,
});
console.log("test complete");
}
test();

566
near/test/testlib.ts Normal file
View File

@ -0,0 +1,566 @@
/**
*
* Copyright 2022 Wormhole Project Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
import {
ChainId,
CHAIN_ID_AVAX,
CHAIN_ID_BSC,
CHAIN_ID_ETH,
CHAIN_ID_FANTOM,
CHAIN_ID_OASIS,
CHAIN_ID_POLYGON,
CHAIN_ID_SOLANA,
CHAIN_ID_TERRA,
CHAIN_ID_NEAR,
} from "@certusone/wormhole-sdk/lib/cjs/utils";
const web3EthAbi = require("web3-eth-abi");
const web3Utils = require("web3-utils");
const elliptic = require("elliptic");
export class TestLib {
zeroBytes: string;
singleGuardianKey: string[] = ["beFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe"];
singleGuardianPrivKey: string[] = [
"cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0",
];
constructor() {
this.zeroBytes =
"0000000000000000000000000000000000000000000000000000000000000000";
}
hexStringToUint8Array(hs: string): Uint8Array {
if (hs.length % 2 === 1) {
// prepend a 0
hs = "0" + hs;
}
const buf = Buffer.from(hs, "hex");
const retval = Uint8Array.from(buf);
return retval;
}
uint8ArrayToHexString(arr: Uint8Array, add0x: boolean) {
const ret: string = Buffer.from(arr).toString("hex");
if (!add0x) {
return ret;
}
return "0x" + ret;
}
guardianKeys: string[] = [
"52A26Ce40F8CAa8D36155d37ef0D5D783fc614d2",
"389A74E8FFa224aeAD0778c786163a7A2150768C",
"B4459EA6482D4aE574305B239B4f2264239e7599",
"072491bd66F63356090C11Aae8114F5372aBf12B",
"51280eA1fd2B0A1c76Ae29a7d54dda68860A2bfF",
"fa9Aa60CfF05e20E2CcAA784eE89A0A16C2057CB",
"e42d59F8FCd86a1c5c4bA351bD251A5c5B05DF6A",
"4B07fF9D5cE1A6ed58b6e9e7d6974d1baBEc087e",
"c8306B84235D7b0478c61783C50F990bfC44cFc0",
"C8C1035110a13fe788259A4148F871b52bAbcb1B",
"58A2508A20A7198E131503ce26bBE119aA8c62b2",
"8390820f04ddA22AFe03be1c3bb10f4ba6CF94A0",
"1FD6e97387C34a1F36DE0f8341E9D409E06ec45b",
"255a41fC2792209CB998A8287204D40996df9E54",
"bA663B12DD23fbF4FbAC618Be140727986B3BBd0",
"79040E577aC50486d0F6930e160A5C75FD1203C6",
"3580D2F00309A9A85efFAf02564Fc183C0183A96",
"3869795913D3B6dBF3B24a1C7654672c69A23c35",
"1c0Cc52D7673c52DE99785741344662F5b2308a0",
];
guardianPrivKeys: string[] = [
"563d8d2fd4e701901d3846dee7ae7a92c18f1975195264d676f8407ac5976757",
"8d97f25916a755df1d9ef74eb4dbebc5f868cb07830527731e94478cdc2b9d5f",
"9bd728ad7617c05c31382053b57658d4a8125684c0098f740a054d87ddc0e93b",
"5a02c4cd110d20a83a7ce8d1a2b2ae5df252b4e5f6781c7855db5cc28ed2d1b4",
"93d4e3b443bf11f99a00901222c032bd5f63cf73fc1bcfa40829824d121be9b2",
"ea40e40c63c6ff155230da64a2c44fcd1f1c9e50cacb752c230f77771ce1d856",
"87eaabe9c27a82198e618bca20f48f9679c0f239948dbd094005e262da33fe6a",
"61ffed2bff38648a6d36d6ed560b741b1ca53d45391441124f27e1e48ca04770",
"bd12a242c6da318fef8f98002efb98efbf434218a78730a197d981bebaee826e",
"20d3597bb16525b6d09e5fb56feb91b053d961ab156f4807e37d980f50e71aff",
"344b313ffbc0199ff6ca08cacdaf5dc1d85221e2f2dc156a84245bd49b981673",
"848b93264edd3f1a521274ca4da4632989eb5303fd15b14e5ec6bcaa91172b05",
"c6f2046c1e6c172497fc23bd362104e2f4460d0f61984938fa16ef43f27d93f6",
"693b256b1ee6b6fb353ba23274280e7166ab3be8c23c203cc76d716ba4bc32bf",
"13c41508c0da03018d61427910b9922345ced25e2bbce50652e939ee6e5ea56d",
"460ee0ee403be7a4f1eb1c63dd1edaa815fbaa6cf0cf2344dcba4a8acf9aca74",
"b25148579b99b18c8994b0b86e4dd586975a78fa6e7ad6ec89478d7fbafd2683",
"90d7ac6a82166c908b8cf1b352f3c9340a8d1f2907d7146fb7cd6354a5436cca",
"b71d23908e4cf5d6cd973394f3a4b6b164eb1065785feee612efdfd8d30005ed",
];
encoder(type: string, val: any) {
if (type == "uint8")
return web3EthAbi.encodeParameter("uint8", val).substring(2 + (64 - 2));
if (type == "uint16")
return web3EthAbi.encodeParameter("uint16", val).substring(2 + (64 - 4));
if (type == "uint32")
return web3EthAbi.encodeParameter("uint32", val).substring(2 + (64 - 8));
if (type == "uint64")
return web3EthAbi.encodeParameter("uint64", val).substring(2 + (64 - 16));
if (type == "uint128")
return web3EthAbi
.encodeParameter("uint128", val)
.substring(2 + (64 - 32));
if (type == "uint256" || type == "bytes32")
return web3EthAbi.encodeParameter(type, val).substring(2 + (64 - 64));
}
ord(c: any) {
return c.charCodeAt(0);
}
genGuardianSetUpgrade(
signers: any,
guardianSet: number,
targetSet: number,
nonce: number,
seq: number,
guardianKeys: string[]
): string {
const b = [
"0x",
this.zeroBytes.slice(0, 28 * 2),
this.encoder("uint8", this.ord("C")),
this.encoder("uint8", this.ord("o")),
this.encoder("uint8", this.ord("r")),
this.encoder("uint8", this.ord("e")),
this.encoder("uint8", 2),
this.encoder("uint16", 0),
this.encoder("uint32", targetSet),
this.encoder("uint8", guardianKeys.length),
];
guardianKeys.forEach((x) => {
b.push(x);
});
let emitter = "0x" + this.zeroBytes.slice(0, 31 * 2) + "04";
let seconds = Math.floor(new Date().getTime() / 1000.0);
return this.createSignedVAA(
guardianSet,
signers,
seconds,
nonce,
1,
emitter,
seq,
32,
b.join("")
);
}
genGSetFee(
signers: any,
guardianSet: number,
nonce: number,
seq: number,
tchain: number,
amt: number
) {
const b = [
"0x",
this.zeroBytes.slice(0, 28 * 2),
this.encoder("uint8", this.ord("C")),
this.encoder("uint8", this.ord("o")),
this.encoder("uint8", this.ord("r")),
this.encoder("uint8", this.ord("e")),
this.encoder("uint8", 3),
this.encoder("uint16", tchain),
this.encoder("uint256", Math.floor(amt)),
];
let emitter = "0x" + this.zeroBytes.slice(0, 31 * 2) + "04";
var seconds = Math.floor(new Date().getTime() / 1000.0);
return this.createSignedVAA(
guardianSet,
signers,
seconds,
nonce,
1,
emitter,
seq,
32,
b.join("")
);
}
genGFeePayout(
signers: any,
guardianSet: number,
nonce: number,
seq: number,
tchain: number,
amt: number,
dest: Uint8Array
) {
const b = [
"0x",
this.zeroBytes.slice(0, 28 * 2),
this.encoder("uint8", this.ord("C")),
this.encoder("uint8", this.ord("o")),
this.encoder("uint8", this.ord("r")),
this.encoder("uint8", this.ord("e")),
this.encoder("uint8", 4),
this.encoder("uint16", tchain),
this.encoder("uint256", Math.floor(amt)),
this.uint8ArrayToHexString(dest, false),
];
let emitter = "0x" + this.zeroBytes.slice(0, 31 * 2) + "04";
var seconds = Math.floor(new Date().getTime() / 1000.0);
return this.createSignedVAA(
guardianSet,
signers,
seconds,
nonce,
1,
emitter,
seq,
32,
b.join("")
);
}
getTokenEmitter(chain: number): string {
if (chain === CHAIN_ID_SOLANA) {
return "c69a1b1a65dd336bf1df6a77afb501fc25db7fc0938cb08595a9ef473265cb4f";
}
if (chain === CHAIN_ID_ETH) {
return "0000000000000000000000003ee18b2214aff97000d974cf647e7c347e8fa585";
}
if (chain === CHAIN_ID_TERRA) {
return "0000000000000000000000007cf7b764e38a0a5e967972c1df77d432510564e2";
}
if (chain === CHAIN_ID_BSC) {
return "000000000000000000000000b6f6d86a8f9879a9c87f643768d9efc38c1da6e7";
}
if (chain === CHAIN_ID_POLYGON) {
return "0000000000000000000000005a58505a96d1dbf8df91cb21b54419fc36e93fde";
}
if (chain === CHAIN_ID_AVAX) {
return "0000000000000000000000000e082f06ff657d94310cb8ce8b0d9a04541d8052";
}
if (chain === CHAIN_ID_OASIS) {
return "0000000000000000000000005848c791e09901b40a9ef749f2a6735b418d7564";
}
if (chain === CHAIN_ID_FANTOM) {
return "0000000000000000000000007C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2";
}
return "";
}
getNftEmitter(chain: ChainId): string {
if (chain === CHAIN_ID_SOLANA) {
return "96ee982293251b48729804c8e8b24b553eb6b887867024948d2236fd37a577ab";
}
if (chain === CHAIN_ID_ETH) {
return "0000000000000000000000006ffd7ede62328b3af38fcd61461bbfc52f5651fe";
}
if (chain === CHAIN_ID_BSC) {
return "0000000000000000000000005a58505a96d1dbf8df91cb21b54419fc36e93fde";
}
if (chain === CHAIN_ID_POLYGON) {
return "00000000000000000000000090bbd86a6fe93d3bc3ed6335935447e75fab7fcf";
}
if (chain === CHAIN_ID_AVAX) {
return "000000000000000000000000f7b6737ca9c4e08ae573f75a97b73d7a813f5de5";
}
if (chain === CHAIN_ID_OASIS) {
return "00000000000000000000000004952D522Ff217f40B5Ef3cbF659EcA7b952a6c1";
}
if (chain === CHAIN_ID_FANTOM) {
return "000000000000000000000000A9c7119aBDa80d4a4E0C06C8F4d8cF5893234535";
}
return "";
}
genRegisterChain(
signers: any,
guardianSet: number,
nonce: number,
seq: number,
chain: number
) {
const b = [
"0x",
this.zeroBytes.slice(0, (32 - 11) * 2),
this.encoder("uint8", this.ord("T")),
this.encoder("uint8", this.ord("o")),
this.encoder("uint8", this.ord("k")),
this.encoder("uint8", this.ord("e")),
this.encoder("uint8", this.ord("n")),
this.encoder("uint8", this.ord("B")),
this.encoder("uint8", this.ord("r")),
this.encoder("uint8", this.ord("i")),
this.encoder("uint8", this.ord("d")),
this.encoder("uint8", this.ord("g")),
this.encoder("uint8", this.ord("e")),
this.encoder("uint8", 1),
this.encoder("uint16", 0),
this.encoder("uint16", chain),
this.getTokenEmitter(chain),
];
let emitter = "0x" + this.zeroBytes.slice(0, 31 * 2) + "04";
var seconds = Math.floor(new Date().getTime() / 1000.0);
return this.createSignedVAA(
guardianSet,
signers,
seconds,
nonce,
1,
emitter,
seq,
32,
b.join("")
);
}
genAssetMeta(
signers: any,
guardianSet: number,
nonce: number,
seq: number,
tokenAddress: string,
chain: number,
decimals: number,
symbol: string,
name: string
) {
const b = [
"0x",
this.encoder("uint8", 2),
this.zeroBytes.slice(0, 64 - tokenAddress.length),
tokenAddress,
this.encoder("uint16", chain),
this.encoder("uint8", decimals),
Buffer.from(symbol).toString("hex"),
this.zeroBytes.slice(0, (32 - symbol.length) * 2),
Buffer.from(name).toString("hex"),
this.zeroBytes.slice(0, (32 - name.length) * 2),
];
// console.log(b.join())
// console.log(b.join('').length)
let emitter = "0x" + this.getTokenEmitter(chain);
let seconds = Math.floor(new Date().getTime() / 1000.0);
return this.createSignedVAA(
guardianSet,
signers,
seconds,
nonce,
chain,
emitter,
seq,
32,
b.join("")
);
}
genTransfer(
signers: any,
guardianSet: number,
nonce: number,
seq: number,
amount: number,
tokenAddress: string,
tokenChain: number,
toAddress: string,
toChain: number,
fee: number
) {
const b = [
"0x",
this.encoder("uint8", 1),
this.encoder("uint256", Math.floor(amount * 100000000)),
this.zeroBytes.slice(0, 64 - tokenAddress.length),
tokenAddress,
this.encoder("uint16", tokenChain),
this.zeroBytes.slice(0, 64 - toAddress.length),
toAddress,
this.encoder("uint16", toChain),
this.encoder("uint256", Math.floor(fee * 100000000)),
];
let emitter = "0x" + this.getTokenEmitter(tokenChain);
let seconds = Math.floor(new Date().getTime() / 1000.0);
return this.createSignedVAA(
guardianSet,
signers,
seconds,
nonce,
tokenChain,
emitter,
seq,
32,
b.join("")
);
}
genNFTTransfer(
signers: any,
guardianSet: number,
nonce: number,
seq: number,
contract: string, // 32 bytes
fchain: number,
symbol: string,
name: string,
tokenid: string,
uri: string,
target: string, // 32 bytes
tochain: number
) {
const b = [
"0x",
this.encoder("uint8", 1),
this.zeroBytes.slice(0, 64 - contract.length),
contract,
this.encoder("uint16", fchain),
Buffer.from(symbol).toString("hex"),
this.zeroBytes.slice(0, (32 - symbol.length) * 2),
Buffer.from(name).toString("hex"),
this.zeroBytes.slice(0, (32 - name.length) * 2),
this.zeroBytes.slice(0, 64 - tokenid.length),
tokenid,
this.encoder("uint8", uri.length),
Buffer.from(uri).toString("hex"),
this.zeroBytes.slice(0, 64 - target.length),
target,
this.encoder("uint16", tochain),
];
let emitter = "0x" + this.getNftEmitter(fchain as ChainId);
let seconds = Math.floor(new Date().getTime() / 1000.0);
return this.createSignedVAA(
guardianSet,
signers,
seconds,
nonce,
fchain,
emitter,
seq,
32,
b.join("")
);
}
/**
* Create a packed and signed VAA for testing.
* See https://github.com/certusone/wormhole/blob/dev.v2/design/0001_generic_message_passing.md
*
* @param {} guardianSetIndex The guardian set index
* @param {*} signers The list of private keys for signing the VAA
* @param {*} timestamp The timestamp of VAA
* @param {*} nonce The nonce.
* @param {*} emitterChainId The emitter chain identifier
* @param {*} emitterAddress The emitter chain address, prefixed with 0x
* @param {*} sequence The sequence.
* @param {*} consistencyLevel The reported consistency level
* @param {*} payload This VAA Payload hex string, prefixed with 0x
*/
createSignedVAA(
guardianSetIndex: number,
signers: any,
timestamp: number,
nonce: number,
emitterChainId: number,
emitterAddress: string,
sequence: number,
consistencyLevel: number,
payload: string
) {
console.log(typeof payload);
const body = [
this.encoder("uint32", timestamp),
this.encoder("uint32", nonce),
this.encoder("uint16", emitterChainId),
this.encoder("bytes32", emitterAddress),
this.encoder("uint64", sequence),
this.encoder("uint8", consistencyLevel),
payload.substring(2),
];
const hash = web3Utils.keccak256(web3Utils.keccak256("0x" + body.join("")));
let signatures = "";
for (const i in signers) {
// eslint-disable-next-line new-cap
const ec = new elliptic.ec("secp256k1");
const key = ec.keyFromPrivate(signers[i]);
const signature = key.sign(hash.substr(2), { canonical: true });
const packSig = [
this.encoder("uint8", i),
this.zeroPadBytes(signature.r.toString(16), 32),
this.zeroPadBytes(signature.s.toString(16), 32),
this.encoder("uint8", signature.recoveryParam),
];
signatures += packSig.join("");
}
const vm = [
this.encoder("uint8", 1),
this.encoder("uint32", guardianSetIndex),
this.encoder("uint8", signers.length),
signatures,
body.join(""),
].join("");
return vm;
}
zeroPadBytes(value: string, length: number) {
while (value.length < 2 * length) {
value = "0" + value;
}
return value;
}
}
module.exports = {
TestLib,
};

164
near/test/watcher.go Normal file
View File

@ -0,0 +1,164 @@
// http --verbose post http://localhost:3030 jsonrpc=2.0 id=dontcare method=block params:='{"block_id": 5004}'
// http --verbose post http://localhost:3030 jsonrpc=2.0 id=dontcare method=chunk params:='{"chunk_id": "AgVjJBCy5LBq9UBJuT1ZEhPJaY8DzUrpxbXGHHkqQkCb"}'
// http --verbose post http://localhost:3030 jsonrpc=2.0 id=dontcare method=EXPERIMENTAL_tx_status params:='["HZsEBFyo5fiRhApFx4SNm7Ao8anfdEiUyS3cVUf8riG1", "7144wormhole.test.near"]'
package main
import (
"bytes"
"fmt"
"github.com/tidwall/gjson"
// "encoding/base64"
"io/ioutil"
"net/http"
"strings"
"time"
// "encoding/json"
)
func getTxStatus(tx string, src string) ([]byte, error) {
s := fmt.Sprintf(`{"id": "dontcare", "jsonrpc": "2.0", "method": "EXPERIMENTAL_tx_status", "params": ["%s", "%s"]}`, tx, src)
fmt.Printf("%s\n", s)
resp, err := http.Post("http://localhost:3030", "application/json", bytes.NewBuffer([]byte(s)))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func getBlock(block uint64) ([]byte, error) {
s := fmt.Sprintf(`{"id": "dontcare", "jsonrpc": "2.0", "method": "block", "params": {"block_id": %d}}`, block)
resp, err := http.Post("http://localhost:3030", "application/json", bytes.NewBuffer([]byte(s)))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func getFinalBlock() ([]byte, error) {
s := fmt.Sprintf(`{"id": "dontcare", "jsonrpc": "2.0", "method": "block", "params": {"finality": "final"}}`)
resp, err := http.Post("http://localhost:3030", "application/json", bytes.NewBuffer([]byte(s)))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func getChunk(chunk string) ([]byte, error) {
s := fmt.Sprintf(`{"id": "dontcare", "jsonrpc": "2.0", "method": "chunk", "params": {"chunk_id": "%s"}}`, chunk)
resp, err := http.Post("http://localhost:3030", "application/json", bytes.NewBuffer([]byte(s)))
if err != nil {
return nil, err
}
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}
func inspectBody(block uint64, body gjson.Result) error {
fmt.Printf("block %d\n", block)
result := body.Get("result.chunks.#.chunk_hash")
if !result.Exists() {
return nil
}
for _, name := range result.Array() {
chunk, err := getChunk(name.String())
if err != nil {
return err
}
txns := gjson.ParseBytes(chunk).Get("result.transactions")
if !txns.Exists() {
continue
}
for _, r := range txns.Array() {
hash := r.Get("hash")
receiver_id := r.Get("receiver_id")
if !hash.Exists() || !receiver_id.Exists() {
continue
}
t, _ := getTxStatus(hash.String(), receiver_id.String())
fmt.Printf("outcome: %s\n", t)
outcomes := gjson.ParseBytes(t).Get("result.receipts_outcome")
if !outcomes.Exists() {
continue
}
for _, o := range outcomes.Array() {
outcome := o.Get("outcome")
if !outcome.Exists() {
continue
}
executor_id := outcome.Get("executor_id")
if !executor_id.Exists() {
continue
}
if executor_id.String() == "wormhole.test.near" {
l := outcome.Get("logs")
if !l.Exists() {
continue
}
for _, log := range l.Array() {
event := log.String()
if !strings.HasPrefix(event, "EVENT_JSON:") {
continue
}
// event_json := gjson.ParseBytes(event[11:])
fmt.Printf("log: %s\n", event[11:])
}
}
}
}
}
return nil
}
func main() {
finalBody, _ := getFinalBlock()
block := gjson.ParseBytes(finalBody).Get("result.chunks.0.height_created").Uint()
for {
finalBody, err := getFinalBlock()
if err != nil {
fmt.Printf(err.Error())
} else {
parsedFinalBody := gjson.ParseBytes(finalBody)
lastBlock := parsedFinalBody.Get("result.chunks.0.height_created").Uint()
for ; block <= lastBlock; block = block + 1 {
if block == lastBlock {
inspectBody(block, parsedFinalBody)
} else {
b, err := getBlock(block)
// fmt.Printf("block: %s\n", b);
if err != nil {
fmt.Printf(err.Error())
break
} else {
inspectBody(block, gjson.ParseBytes(b))
}
}
}
}
time.Sleep(time.Second)
}
}

View File

@ -203,6 +203,12 @@
"tokenBridgeEmitterAddress": "8edf5b0e108c3a1a0a4b704cc89591f2ad8d50df24e991567e640ed720a94be2"
}
},
"15": {
"contracts": {
"tokenBridgeEmitterAddress": "e83c99874cb2d60921648a438606f5ffcf60c2e26ef13678b2e57fab3def6a30",
"nftBridgeEmitterAddress": "11aaad1c851095bf97ee1be9ed1161a47187aba8b71bf6821efac391d8ee95f7"
}
},
"18": {
"rpcUrlTilt": "http://terra2-terrad:1317",
"rpcUrlLocal": "http://localhost:1318",

View File

@ -85,11 +85,13 @@ ethTokenBridge=$(jq --raw-output '.chains."2".contracts.tokenBridgeEmitterAddres
terraTokenBridge=$(jq --raw-output '.chains."3".contracts.tokenBridgeEmitterAddress' $addressesJson)
bscTokenBridge=$(jq --raw-output '.chains."4".contracts.tokenBridgeEmitterAddress' $addressesJson)
algoTokenBridge=$(jq --raw-output '.chains."8".contracts.tokenBridgeEmitterAddress' $addressesJson)
nearTokenBridge=$(jq --raw-output '.chains."15".contracts.tokenBridgeEmitterAddress' $addressesJson)
terra2TokenBridge=$(jq --raw-output '.chains."18".contracts.tokenBridgeEmitterAddress' $addressesJson)
solNFTBridge=$(jq --raw-output '.chains."1".contracts.nftBridgeEmitterAddress' $addressesJson)
ethNFTBridge=$(jq --raw-output '.chains."2".contracts.nftBridgeEmitterAddress' $addressesJson)
terraNFTBridge=$(jq --raw-output '.chains."3".contracts.nftBridgeEmitterAddress' $addressesJson)
nearNFTBridge=$(jq --raw-output '.chains."15".contracts.nftBridgeEmitterAddress' $addressesJson)
# 4) create token bridge registration VAAs
# invoke CLI commands to create registration VAAs
@ -98,6 +100,7 @@ ethTokenBridgeVAA=$(node ./clients/js/build/main.js generate registration -m Tok
terraTokenBridgeVAA=$(node ./clients/js/build/main.js generate registration -m TokenBridge -c terra -a ${terraTokenBridge} -g ${guardiansPrivateCSV})
bscTokenBridgeVAA=$(node ./clients/js/build/main.js generate registration -m TokenBridge -c bsc -a ${bscTokenBridge} -g ${guardiansPrivateCSV})
algoTokenBridgeVAA=$(node ./clients/js/build/main.js generate registration -m TokenBridge -c algorand -a ${algoTokenBridge} -g ${guardiansPrivateCSV})
nearTokenBridgeVAA=$(node ./clients/js/build/main.js generate registration -m TokenBridge -c near -a ${nearTokenBridge} -g ${guardiansPrivateCSV})
terra2TokenBridgeVAA=$(node ./clients/js/build/main.js generate registration -m TokenBridge -c terra2 -a ${terra2TokenBridge} -g ${guardiansPrivateCSV})
@ -106,7 +109,7 @@ echo "generating contract registration VAAs for nft bridges"
solNFTBridgeVAA=$(node ./clients/js/build/main.js generate registration -m NFTBridge -c solana -a ${solNFTBridge} -g ${guardiansPrivateCSV})
ethNFTBridgeVAA=$(node ./clients/js/build/main.js generate registration -m NFTBridge -c ethereum -a ${ethNFTBridge} -g ${guardiansPrivateCSV})
terraNFTBridgeVAA=$(node ./clients/js/build/main.js generate registration -m NFTBridge -c terra -a ${terraNFTBridge} -g ${guardiansPrivateCSV})
nearNFTBridgeVAA=$(node ./clients/js/build/main.js generate registration -m NFTBridge -c near -a ${nearNFTBridge} -g ${guardiansPrivateCSV})
# 6) write the registration VAAs to env files
@ -118,10 +121,12 @@ terraTokenBridge="REGISTER_TERRA_TOKEN_BRIDGE_VAA"
bscTokenBridge="REGISTER_BSC_TOKEN_BRIDGE_VAA"
algoTokenBridge="REGISTER_ALGO_TOKEN_BRIDGE_VAA"
terra2TokenBridge="REGISTER_TERRA2_TOKEN_BRIDGE_VAA"
nearTokenBridge="REGISTER_NEAR_TOKEN_BRIDGE_VAA"
solNFTBridge="REGISTER_SOL_NFT_BRIDGE_VAA"
ethNFTBridge="REGISTER_ETH_NFT_BRIDGE_VAA"
terraNFTBridge="REGISTER_TERRA_NFT_BRIDGE_VAA"
nearNFTBridge="REGISTER_NEAR_NFT_BRIDGE_VAA"
# solana token bridge
@ -160,6 +165,12 @@ upsert_env_file $envFile $algoTokenBridge $algoTokenBridgeVAA
upsert_env_file $ethFile $terra2TokenBridge $terra2TokenBridgeVAA
upsert_env_file $envFile $terra2TokenBridge $terra2TokenBridgeVAA
# near token bridge
upsert_env_file $ethFile $nearTokenBridge $nearTokenBridgeVAA
upsert_env_file $envFile $nearTokenBridge $nearTokenBridgeVAA
# near nft bridge
upsert_env_file $ethFile $nearNFTBridge $nearNFTBridgeVAA
upsert_env_file $envFile $nearNFTBridge $nearNFTBridgeVAA
# 7) copy the local .env file to the solana & terra dirs, if the script is running on the host machine
# chain dirs will not exist if running in docker for Tilt, only if running locally. check before copying.
@ -170,7 +181,7 @@ if [[ -d ./ethereum ]]; then
fi
# copy the hex envFile to each of the non-EVM chains
for envDest in ./solana/.env ./terra/tools/.env ./cosmwasm/tools/.env ./algorand/.env; do
for envDest in ./solana/.env ./terra/tools/.env ./cosmwasm/tools/.env ./algorand/.env ./near/.env; do
dirname=$(dirname $envDest)
if [[ -d "$dirname" ]]; then
echo "copying $envFile to $envDest"