sdk/js: aptos (#1736)

* sdk/js: aptos

* sdk/aptos: change api interface to be more flexible

sdk/aptos: add attestToken

sdk/aptos: added createdWrapped

sdk/aptos: add getForeignAsset

sdk/aptos: stricter sanity check for fully qualified type

sdk/aptos: ensure addresses are left padded

sdk/aptos: check if asset exists in getForeignAsset

sdk/aptos: stricter sanity check - hex prefix can't be capital

sdk/aptos: add updatewrapped

sdk/aptos: update readme with token attestation example

sdk/aptos: added transfer

sdk/aptos: add getIsTransferCompleted

sdk/aptos: add isWrappedAsset and getOriginalAsset

sdk/aptos: add redeem

sdk/aptos: make init tokenbridge entry func

* sdk/aptos: separated signing/submitting txs from creating raw txs

* clients/js: hash aptos fully qualified type to get address

* sdk/aptos: return payload from api instead of rawtx

* sdk/aptos: derive token info from vaa

* sdk/aptos: fix getAssetFullyQualifiedType for native asset

* sdk/aptos: add min gas price

* sdk/aptos: bump aptos version

* sdk/aptos: dont require 0x in front of addresses

* sdk/aptos: progress on e2e tests

* sdk/aptos: upgrade resource address derivation

This was changed recently
25696fd266/aptos-move/framework/aptos-framework/sources/account.move (L90-L95)

* sdk/js: fix getForeignAssetAptos

* sdk/js: update testnet aptos address

* sdk/js: update aptos entry functions

* sdk/aptos: fix parsesequencefromlog

* sdk/aptos: throw errors instead of string literal

* sdk/aptos: update testnet/mainnet addresses

* sdk/aptos: fix  completeTransfer and getOriginalAsset

* sdk/aptos: update transferTokens to take in type and remove wormholeFee param

* sdk/aptos: add typeToExternalAddress utility

* sdk/js: update parseSequenceFromLogAptos

* sdk/js: test version bump again

* sdk/aptos: make transfer param type consistent

* sdk/aptos: test transfer to another chain test done

* sdk/aptos: use completeTransferAndRegister

* sdk/aptos: allow tryNativeToHexString to take in account addresses

* sdk/aptos: finish e2e tests

* sdk/aptos: test all apis

* sdk/aptos: add registerCoin utility

* sdk/js: utility to submit script bytecode to chain

* sdk/aptos: update test to be idempotent

* sdk/aptos: stricter check on aptos type

* clients/js: remove unused imports from rebase

* sdk/aptos: change node and faucet urls in ci

Co-authored-by: aki <akshath@live.com>
Co-authored-by: Evan Gray <battledingo@gmail.com>
This commit is contained in:
Csongor Kiss 2022-10-24 23:12:02 +01:00 committed by GitHub
parent 91bd9a5c36
commit eaa5107b33
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1406 additions and 109 deletions

View File

@ -1,6 +1,7 @@
# Wormhole CLI
This tool is a command line interface to Wormhole.
## Installation
make install
@ -12,7 +13,7 @@ private keys, based on `.env.sample` in this folder.
## Usage
``` sh
```sh
worm [command]
Commands:
@ -31,19 +32,32 @@ Options:
--version Show version number [boolean]
```
Consult the `--help` flag for using subcommands.
Consult the `--help` flag for using subcommands.
### VAA generation
### VAA generation
Use `generate` to create VAAs for testing. For example, to create an NFT bridge registration VAA:
Use `generate` to create VAAs for testing. For example, to create an NFT bridge registration VAA:
``` sh
```sh
$ worm generate registration --module NFTBridge \
--chain bsc \
--contract-address 0x706abc4E45D419950511e474C7B9Ed348A4a716c \
--guardian-secret cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
```
Example creating a token attestation VAA:
```sh
$ worm generate attestation --emitter-chain ethereum \
--emitter-address 11111111111111111111111111111115 \
--chain ethereum \
--token-address 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 \
--decimals 6 \
--symbol USDC \
--name USDC \
--guardian-secret cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
```
### VAA parsing
Use `parse` to parse a VAA into JSON. For example,
@ -52,7 +66,7 @@ Use `parse` to parse a VAA into JSON. For example,
will fetch governance VAA `13940208096455381020` and print it as JSON.
``` sh
```sh
# ...signatures elided
timestamp: 1651416474,
nonce: 1570649151,
@ -97,12 +111,11 @@ what's the destination chain and module. For example, a contract upgrade contain
worm submit $(cat my-nft-registration.txt) --network mainnet
For VAAs that don't have a specific target chain (like registrations or guardian
set upgrades), the script will ask you to specify the target chain.
For example, to submit a guardian set upgrade on all chains, simply run:
``` sh
```sh
$ worm-fetch-governance 13940208096455381020 > guardian-upgrade.txt
$ worm submit $(cat guardian-upgrade.txt) --network mainnet --chain oasis
$ worm submit $(cat guardian-upgrade.txt) --network mainnet --chain aurora
@ -121,12 +134,11 @@ $ worm submit $(cat guardian-upgrade.txt) --network mainnet --chain celo
The VAA payload type (guardian set upgrade) specifies that this VAA should go to the core bridge, and the tool directs it there.
### info
To get info about a contract (only EVM supported at this time)
``` sh
```sh
$ worm evm info -c bsc -n mainnet -m TokenBridge
{
@ -169,6 +181,7 @@ $ worm evm info -c bsc -n mainnet -m TokenBridge
}
```
### Misc
To get the contract address for a module:
@ -178,4 +191,3 @@ To get the contract address for a module:
To get the RPC address for a chain
$ worm rpc mainnet bsc

View File

@ -27,6 +27,7 @@ import { impossible, Payload, serialiseVAA, VAA } from "./vaa";
import { ethers } from "ethers";
import { NETWORKS } from "./networks";
import base58 from "bs58";
import { sha3_256 } from "js-sha3";
import { isOutdated } from "./cmds/update";
import { setDefaultWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm";
import { assertChain, assertEVMChain, ChainName, CHAINS, CONTRACTS as SDK_CONTRACTS, isCosmWasmChain, isEVMChain, isTerraChain, toChainId, toChainName } from "@certusone/wormhole-sdk/lib/cjs/utils/consts";
@ -923,8 +924,11 @@ function parseAddress(chain: ChainName, address: string): string {
} else if (chain === "sui") {
throw Error("SUI is not supported yet");
} else if (chain === "aptos") {
// TODO: is there a better native format for aptos?
if (/^(0x)?[0-9a-fA-F]+$/.test(address)) {
return "0x" + evm_address(address);
}
return sha3_256(Buffer.from(address)); // address is hash of fully qualified type
} else if (chain === "wormholechain" || (chain + "") == "wormchain") {
// TODO: update this condition after ChainName is updated to remove "wormholechain"
const sdk = require("@certusone/wormhole-sdk/lib/cjs/utils/array")

125
sdk/js/package-lock.json generated
View File

@ -17,6 +17,7 @@
"@terra-money/terra.js": "^3.1.3",
"@xpla/xpla.js": "^0.2.1",
"algosdk": "^1.15.0",
"aptos": "^1.3.16",
"axios": "^0.24.0",
"bech32": "^2.0.0",
"js-base64": "^3.6.1",
@ -2496,10 +2497,9 @@
}
},
"node_modules/@noble/hashes": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz",
"integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==",
"dev": true,
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz",
"integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A==",
"funding": [
{
"type": "individual",
@ -2568,6 +2568,32 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"node_modules/@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
]
},
"node_modules/@scure/bip39": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz",
"integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==",
"funding": [
{
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
],
"dependencies": {
"@noble/hashes": "~1.1.1",
"@scure/base": "~1.1.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@ -3353,6 +3379,43 @@
"node": ">= 8"
}
},
"node_modules/aptos": {
"version": "1.3.16",
"resolved": "https://registry.npmjs.org/aptos/-/aptos-1.3.16.tgz",
"integrity": "sha512-LxI4XctQ5VeL+HokjwuGPwsb1fcydLIn4agFXyhn7hSYosTLNRxQ3UIixyP4Fmv6qPBjQVu8hELVSlThQk/EjA==",
"dependencies": {
"@noble/hashes": "1.1.3",
"@scure/bip39": "1.1.0",
"axios": "0.27.2",
"form-data": "4.0.0",
"tweetnacl": "1.0.3"
},
"engines": {
"node": ">=11.0.0"
}
},
"node_modules/aptos/node_modules/axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"dependencies": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"node_modules/aptos/node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
@ -15368,10 +15431,9 @@
}
},
"@noble/hashes": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.2.tgz",
"integrity": "sha512-KYRCASVTv6aeUi1tsF8/vpyR7zpfs3FUzy2Jqm+MU+LmUKhQ0y2FpfwqkCcxSg2ua4GALJd8k2R76WxwZGbQpA==",
"dev": true
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.1.3.tgz",
"integrity": "sha512-CE0FCR57H2acVI5UOzIGSSIYxZ6v/HOhDR0Ro9VLyhnzLwx0o8W1mmgaqlEUx4049qJDlIBRztv5k+MM8vbO3A=="
},
"@openzeppelin/contracts": {
"version": "4.2.0",
@ -15433,6 +15495,20 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA="
},
"@scure/base": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA=="
},
"@scure/bip39": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.1.0.tgz",
"integrity": "sha512-pwrPOS16VeTKg98dYXQyIjJEcWfz7/1YJIwxUEPFfQPtc86Ym/1sVgQ2RLoD43AazMk2l/unK4ITySSpW2+82w==",
"requires": {
"@noble/hashes": "~1.1.1",
"@scure/base": "~1.1.0"
}
},
"@sindresorhus/is": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz",
@ -16092,6 +16168,39 @@
"picomatch": "^2.0.4"
}
},
"aptos": {
"version": "1.3.16",
"resolved": "https://registry.npmjs.org/aptos/-/aptos-1.3.16.tgz",
"integrity": "sha512-LxI4XctQ5VeL+HokjwuGPwsb1fcydLIn4agFXyhn7hSYosTLNRxQ3UIixyP4Fmv6qPBjQVu8hELVSlThQk/EjA==",
"requires": {
"@noble/hashes": "1.1.3",
"@scure/bip39": "1.1.0",
"axios": "0.27.2",
"form-data": "4.0.0",
"tweetnacl": "1.0.3"
},
"dependencies": {
"axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
"integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==",
"requires": {
"follow-redirects": "^1.14.9",
"form-data": "^4.0.0"
}
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",

View File

@ -1,6 +1,6 @@
{
"name": "@certusone/wormhole-sdk",
"version": "0.7.2",
"version": "0.7.6",
"description": "SDK for interacting with Wormhole",
"homepage": "https://wormhole.com",
"main": "./lib/cjs/index.js",
@ -66,6 +66,7 @@
"@terra-money/terra.js": "^3.1.3",
"@xpla/xpla.js": "^0.2.1",
"algosdk": "^1.15.0",
"aptos": "^1.3.16",
"axios": "^0.24.0",
"bech32": "^2.0.0",
"js-base64": "^3.6.1",

View File

@ -0,0 +1,39 @@
import { Types } from "aptos";
// Contract upgrade
export const authorizeUpgrade = (
address: string,
vaa: Uint8Array
): Types.EntryFunctionPayload => {
if (!address) throw new Error("Need bridge address.");
return {
function: `${address}::contract_upgrade::submit_vaa_entry`,
type_arguments: [],
arguments: [vaa],
};
};
export const upgradeContract = (
address: string,
metadataSerialized: Uint8Array,
code: Array<Uint8Array>
): Types.EntryFunctionPayload => {
if (!address) throw new Error("Need bridge address.");
return {
function: `${address}::contract_upgrade::upgrade`,
type_arguments: [],
arguments: [metadataSerialized, code],
};
};
export const migrateContract = (
address: string
): Types.EntryFunctionPayload => {
if (!address) throw new Error("Need bridge address.");
return {
function: `${address}::contract_upgrade::migrate`,
type_arguments: [],
arguments: [],
};
};

View File

@ -0,0 +1,33 @@
import { Types } from "aptos";
import { ChainId } from "../../utils";
// Guardian set upgrade
export const upgradeGuardianSet = (
coreBridgeAddress: string,
vaa: Uint8Array,
): Types.EntryFunctionPayload => {
if (!coreBridgeAddress) throw new Error("Need core bridge address.");
return {
function: `${coreBridgeAddress}::guardian_set_upgrade::submit_vaa_entry`,
type_arguments: [],
arguments: [vaa],
};
};
// Init WH
export const initWormhole = (
coreBridgeAddress: string,
chainId: ChainId,
governanceChainId: number,
governanceContract: Uint8Array,
initialGuardian: Uint8Array,
): Types.EntryFunctionPayload => {
if (!coreBridgeAddress) throw new Error("Need core bridge address.");
return {
function: `${coreBridgeAddress}::wormhole::init`,
type_arguments: [],
arguments: [chainId, governanceChainId, governanceContract, initialGuardian],
};
};

View File

@ -0,0 +1,241 @@
import { AptosClient, TxnBuilderTypes, Types } from "aptos";
import { _parseVAAAlgorand } from "../../algorand";
import {
assertChain,
ChainId,
ChainName,
CHAIN_ID_APTOS,
coalesceChainId,
getAssetFullyQualifiedType,
getTypeFromExternalAddress,
hexToUint8Array,
isValidAptosType,
} from "../../utils";
// Attest token
export const attestToken = (
tokenBridgeAddress: string,
tokenChain: ChainId | ChainName,
tokenAddress: string,
): Types.EntryFunctionPayload => {
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
const assetType = getAssetFullyQualifiedType(
tokenBridgeAddress,
coalesceChainId(tokenChain),
tokenAddress,
);
if (!assetType) throw new Error("Invalid asset address.");
return {
function: `${tokenBridgeAddress}::attest_token::attest_token_entry`,
type_arguments: [assetType],
arguments: [],
};
};
// Complete transfer
export const completeTransfer = async (
client: AptosClient,
tokenBridgeAddress: string,
transferVAA: Uint8Array,
feeRecipient: string,
): Promise<Types.EntryFunctionPayload> => {
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
const parsedVAA = _parseVAAAlgorand(transferVAA);
if (!parsedVAA.FromChain || !parsedVAA.Contract || !parsedVAA.ToChain) {
throw new Error("VAA does not contain required information");
}
if (parsedVAA.ToChain !== CHAIN_ID_APTOS) {
throw new Error("Transfer is not destined for Aptos");
}
assertChain(parsedVAA.FromChain);
const assetType =
parsedVAA.FromChain === CHAIN_ID_APTOS
? await getTypeFromExternalAddress(
client,
tokenBridgeAddress,
parsedVAA.Contract
)
: getAssetFullyQualifiedType(
tokenBridgeAddress,
coalesceChainId(parsedVAA.FromChain),
parsedVAA.Contract
);
if (!assetType) throw new Error("Invalid asset address.");
return {
function: `${tokenBridgeAddress}::complete_transfer::submit_vaa_entry`,
type_arguments: [assetType],
arguments: [transferVAA, feeRecipient],
};
};
export const completeTransferAndRegister = async (
client: AptosClient,
tokenBridgeAddress: string,
transferVAA: Uint8Array,
): Promise<Types.EntryFunctionPayload> => {
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
const parsedVAA = _parseVAAAlgorand(transferVAA);
if (!parsedVAA.FromChain || !parsedVAA.Contract || !parsedVAA.ToChain) {
throw new Error("VAA does not contain required information");
}
if (parsedVAA.ToChain !== CHAIN_ID_APTOS) {
throw new Error("Transfer is not destined for Aptos");
}
assertChain(parsedVAA.FromChain);
const assetType =
parsedVAA.FromChain === CHAIN_ID_APTOS
? await getTypeFromExternalAddress(
client,
tokenBridgeAddress,
parsedVAA.Contract
)
: getAssetFullyQualifiedType(
tokenBridgeAddress,
coalesceChainId(parsedVAA.FromChain),
parsedVAA.Contract
);
if (!assetType) throw new Error("Invalid asset address.");
return {
function: `${tokenBridgeAddress}::complete_transfer::submit_vaa_and_register_entry`,
type_arguments: [assetType],
arguments: [transferVAA],
};
};
export const completeTransferWithPayload = (
_tokenBridgeAddress: string,
_tokenChain: ChainId | ChainName,
_tokenAddress: string,
_vaa: Uint8Array,
): Types.EntryFunctionPayload => {
throw new Error("Completing transfers with payload is not yet supported in the sdk");
};
export const registerCoin = (
tokenBridgeAddress: string,
originChain: ChainId | ChainName,
originAddress: string
): TxnBuilderTypes.TransactionPayloadScript => {
const bytecode = hexToUint8Array(
"a11ceb0b050000000601000403041104150405190b072436085a200000000101020002000003020401000004000101000103020301060c000105010900010104636f696e067369676e65720a616464726573735f6f661569735f6163636f756e745f726567697374657265640872656769737465720000000000000000000000000000000000000000000000000000000000000001010000010c0a001100380020030605090b003801050b0b000102"
);
const assetType = getAssetFullyQualifiedType(
tokenBridgeAddress,
coalesceChainId(originChain),
originAddress
);
if (!assetType) throw new Error("Asset type is null");
const typeTag = new TxnBuilderTypes.TypeTagStruct(
TxnBuilderTypes.StructTag.fromString(assetType)
);
return new TxnBuilderTypes.TransactionPayloadScript(
new TxnBuilderTypes.Script(bytecode, [typeTag], [])
);
};
// Deploy coin
// don't need `signer` and `&signer` in argument list because the Move VM will inject them
export const deployCoin = (tokenBridgeAddress: string): Types.EntryFunctionPayload => {
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
return {
function: `${tokenBridgeAddress}::deploy_coin::deploy_coin`,
type_arguments: [],
arguments: [],
};
};
// Register chain
export const registerChain = (
tokenBridgeAddress: string,
vaa: Uint8Array,
): Types.EntryFunctionPayload => {
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
return {
function: `${tokenBridgeAddress}::register_chain::submit_vaa_entry`,
type_arguments: [],
arguments: [vaa],
};
};
// Transfer tokens
export const transferTokens = (
tokenBridgeAddress: string,
fullyQualifiedType: string,
amount: string,
recipientChain: ChainId | ChainName,
recipient: Uint8Array,
relayerFee: string,
nonce: number,
payload: string = "",
): Types.EntryFunctionPayload => {
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
if (!isValidAptosType(fullyQualifiedType)) {
throw new Error("Need fully qualified address");
}
const recipientChainId = coalesceChainId(recipientChain);
if (payload) {
throw new Error("Transfer with payload are not yet supported in the sdk");
} else {
return {
function: `${tokenBridgeAddress}::transfer_tokens::transfer_tokens_entry`,
type_arguments: [fullyQualifiedType],
arguments: [amount, recipientChainId, recipient, relayerFee, nonce],
};
}
};
// Created wrapped coin
export const createWrappedCoinType = (
tokenBridgeAddress: string,
vaa: Uint8Array,
): Types.EntryFunctionPayload => {
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
return {
function: `${tokenBridgeAddress}::wrapped::create_wrapped_coin_type`,
type_arguments: [],
arguments: [vaa],
};
};
export const createWrappedCoin = (
tokenBridgeAddress: string,
attestVAA: Uint8Array
): Types.EntryFunctionPayload => {
if (!tokenBridgeAddress) throw new Error("Need token bridge address.");
const parsedVAA = _parseVAAAlgorand(attestVAA);
if (!parsedVAA.FromChain || !parsedVAA.Contract) {
throw new Error("VAA does not contain required information");
}
assertChain(parsedVAA.FromChain);
const assetType = getAssetFullyQualifiedType(
tokenBridgeAddress,
coalesceChainId(parsedVAA.FromChain),
parsedVAA.Contract
);
if (!assetType) throw new Error("Invalid asset address.");
return {
function: `${tokenBridgeAddress}::wrapped::create_wrapped_coin`,
type_arguments: [assetType],
arguments: [attestVAA],
};
};

View File

@ -0,0 +1,3 @@
export * from "./api/common";
export * from "./api/coreBridge";
export * from "./api/tokenBridge";

38
sdk/js/src/aptos/types.ts Normal file
View File

@ -0,0 +1,38 @@
export type State = {
consumed_vaas: {
elems: {
handle: string;
};
};
emitter_cap: {
emitter: string;
sequence: string;
};
governance_chain_id: {
number: string;
};
governance_contract: {
external_address: string;
};
native_infos: {
handle: string;
};
registered_emitters: {
handle: string;
};
signer_cap: {
account: string;
};
wrapped_infos: {
handle: string;
};
};
export type OriginInfo = {
token_address: {
external_address: string;
};
token_chain: {
number: string; // lol
};
};

View File

@ -1,6 +1,7 @@
import { TransactionResponse } from "@solana/web3.js";
import { TxInfo } from "@terra-money/terra.js";
import { TxInfo as XplaTxInfo } from "@xpla/xpla.js";
import { AptosClient, Types } from "aptos";
import { BigNumber, ContractReceipt } from "ethers";
import { FinalExecutionOutcome } from "near-api-js/lib/providers";
import { Implementation__factory } from "../ethers-contracts";
@ -159,3 +160,23 @@ export function parseSequenceFromLogNear(
}
return null;
}
/**
* Given a transaction result, return the first WormholeMessage event sequence
* @param coreBridgeAddress Wormhole Core bridge address
* @param result the result of client.waitForTransactionWithResult(txHash)
* @returns sequence
*/
export function parseSequenceFromLogAptos(
coreBridgeAddress: string,
result: Types.UserTransaction
): string | null {
if (result.success) {
const event = result.events.find(
(e) => e.type === `${coreBridgeAddress}::state::WormholeMessage`
);
return event?.data.sequence || null;
}
return null;
}

View File

@ -0,0 +1,410 @@
import { describe, expect, jest, test } from "@jest/globals";
import {
AptosAccount,
AptosClient,
FaucetClient,
HexString,
Types,
} from "aptos";
import {
approveEth,
APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
attestFromAptos,
attestFromEth,
CHAIN_ID_APTOS,
CHAIN_ID_ETH,
CONTRACTS,
createWrappedOnAptos,
createWrappedOnEth,
createWrappedTypeOnAptos,
getAssetFullyQualifiedType,
getEmitterAddressEth,
getExternalAddressFromType,
getForeignAssetAptos,
getForeignAssetEth,
getIsTransferCompletedAptos,
getIsTransferCompletedEth,
getIsWrappedAssetAptos,
getOriginalAssetAptos,
getSignedVAAWithRetry,
hexToUint8Array,
redeemOnAptos,
redeemOnEth,
signAndSubmitEntryFunction,
signAndSubmitScript,
TokenImplementation__factory,
transferFromAptos,
transferFromEth,
tryNativeToHexString,
tryNativeToUint8Array,
uint8ArrayToHex,
} from "../..";
import { setDefaultWasm } from "../../solana/wasm";
import {
APTOS_FAUCET_URL,
APTOS_NODE_URL,
APTOS_PRIVATE_KEY,
ETH_NODE_URL,
ETH_PRIVATE_KEY3,
TEST_ERC20,
WORMHOLE_RPC_HOSTS,
} from "./consts";
import {
parseSequenceFromLogAptos,
parseSequenceFromLogEth,
} from "../../bridge/parseSequenceFromLog";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { ethers } from "ethers";
import { parseUnits } from "ethers/lib/utils";
import { registerCoin } from "../../aptos";
setDefaultWasm("node");
const JEST_TEST_TIMEOUT = 60000;
jest.setTimeout(JEST_TEST_TIMEOUT);
describe("Aptos SDK tests", () => {
test("Transfer native token from Aptos to Ethereum", async () => {
// setup aptos
const client = new AptosClient(APTOS_NODE_URL);
const faucet = new FaucetClient(APTOS_NODE_URL, APTOS_FAUCET_URL);
const sender = new AptosAccount(hexToUint8Array(APTOS_PRIVATE_KEY));
const aptosTokenBridge = CONTRACTS.DEVNET.aptos.token_bridge;
const aptosCoreBridge = CONTRACTS.DEVNET.aptos.core;
// sanity check funds in the account
const COIN_TYPE = "0x1::aptos_coin::AptosCoin";
const before = await getBalanceAptos(client, COIN_TYPE, sender.address());
await faucet.fundAccount(sender.address(), 100_000_000);
const after = await getBalanceAptos(client, COIN_TYPE, sender.address());
expect(Number(after) - Number(before)).toEqual(100_000_000);
// attest native aptos token
const attestPayload = attestFromAptos(
aptosTokenBridge,
CHAIN_ID_APTOS,
COIN_TYPE
);
let tx = (await signAndSubmitEntryFunction(
client,
sender,
attestPayload
)) as Types.UserTransaction;
// get signed attest vaa
let sequence = parseSequenceFromLogAptos(aptosCoreBridge, tx);
expect(sequence).toBeTruthy();
const { vaaBytes: attestVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_APTOS,
APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
sequence!,
{
transport: NodeHttpTransport(),
},
1000,
5
);
expect(attestVAA).toBeTruthy();
// setup ethereum
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
const recipient = new ethers.Wallet(ETH_PRIVATE_KEY3, provider);
const recipientAddress = await recipient.getAddress();
const ethTokenBridge = CONTRACTS.DEVNET.ethereum.token_bridge;
try {
await createWrappedOnEth(ethTokenBridge, recipient, attestVAA);
} catch (e) {
// this could fail because the token is already attested (in an unclean env)
}
// check attestation on ethereum
const externalAddress = hexToUint8Array(
await getExternalAddressFromType(COIN_TYPE)
);
const address = getForeignAssetEth(
ethTokenBridge,
provider,
CHAIN_ID_APTOS,
externalAddress
);
expect(address).toBeTruthy();
expect(address).not.toBe(ethers.constants.AddressZero);
// transfer from aptos
const balanceBeforeTransferAptos = ethers.BigNumber.from(
await getBalanceAptos(client, COIN_TYPE, sender.address())
);
const transferPayload = transferFromAptos(
aptosTokenBridge,
COIN_TYPE,
(10_000_000).toString(),
CHAIN_ID_ETH,
tryNativeToUint8Array(recipientAddress, CHAIN_ID_ETH)
);
tx = (await signAndSubmitEntryFunction(
client,
sender,
transferPayload
)) as Types.UserTransaction;
const balanceAfterTransferAptos = ethers.BigNumber.from(
await getBalanceAptos(client, COIN_TYPE, sender.address())
);
expect(
balanceBeforeTransferAptos
.sub(balanceAfterTransferAptos)
.gt((10_000_000).toString())
).toBe(true);
// get signed transfer vaa
sequence = parseSequenceFromLogAptos(aptosCoreBridge, tx);
expect(sequence).toBeTruthy();
const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_APTOS,
APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
sequence!,
{
transport: NodeHttpTransport(),
},
1000,
5
);
expect(transferVAA).toBeTruthy();
// get balance on eth
const originAssetHex = tryNativeToUint8Array(COIN_TYPE, CHAIN_ID_APTOS);
if (!originAssetHex) {
throw new Error("originAssetHex is null");
}
const foreignAsset = await getForeignAssetEth(
ethTokenBridge,
provider,
CHAIN_ID_APTOS,
originAssetHex
);
if (!foreignAsset) {
throw new Error("foreignAsset is null");
}
const balanceBeforeTransferEth = await getBalanceEth(
foreignAsset,
recipient
);
// redeem on eth
await redeemOnEth(ethTokenBridge, recipient, transferVAA);
expect(
await getIsTransferCompletedEth(ethTokenBridge, provider, transferVAA)
).toBe(true);
const balanceAfterTransferEth = await getBalanceEth(
foreignAsset,
recipient
);
expect(
balanceAfterTransferEth.sub(balanceBeforeTransferEth).toNumber()
).toEqual(10_000_000);
// clean up
provider.destroy();
});
test("Transfer native ERC-20 from Ethereum to Aptos", async () => {
// setup ethereum
const provider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
const sender = new ethers.Wallet(ETH_PRIVATE_KEY3, provider);
const ethTokenBridge = CONTRACTS.DEVNET.ethereum.token_bridge;
const ethCoreBridge = CONTRACTS.DEVNET.ethereum.core;
// attest from eth
const attestReceipt = await attestFromEth(
ethTokenBridge,
sender,
TEST_ERC20
);
// get signed attest vaa
let sequence = parseSequenceFromLogEth(attestReceipt, ethCoreBridge);
expect(sequence).toBeTruthy();
const { vaaBytes: attestVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_ETH,
getEmitterAddressEth(ethTokenBridge),
sequence,
{
transport: NodeHttpTransport(),
},
1000,
5
);
expect(attestVAA).toBeTruthy();
// setup aptos
const client = new AptosClient(APTOS_NODE_URL);
const recipient = new AptosAccount(hexToUint8Array(APTOS_PRIVATE_KEY));
const aptosTokenBridge = CONTRACTS.DEVNET.aptos.token_bridge;
const createWrappedCoinTypePayload = createWrappedTypeOnAptos(
aptosTokenBridge,
attestVAA
);
try {
await signAndSubmitEntryFunction(
client,
recipient,
createWrappedCoinTypePayload
);
} catch (e) {
// only throw if token has not been attested but this call fails
if (
!(
new Error(e).message.includes("ECOIN_INFO_ALREADY_PUBLISHED") ||
new Error(e).message.includes("ERESOURCE_ACCCOUNT_EXISTS")
)
) {
throw e;
}
}
const createWrappedCoinPayload = createWrappedOnAptos(
aptosTokenBridge,
attestVAA
);
try {
await signAndSubmitEntryFunction(
client,
recipient,
createWrappedCoinPayload
);
} catch (e) {
// only throw if token has not been attested but this call fails
if (
!(
new Error(e).message.includes("ECOIN_INFO_ALREADY_PUBLISHED") ||
new Error(e).message.includes("ERESOURCE_ACCCOUNT_EXISTS")
)
) {
throw e;
}
}
// check attestation on aptos
const aptosWrappedAddress = await getForeignAssetAptos(
client,
aptosTokenBridge,
CHAIN_ID_ETH,
TEST_ERC20
);
if (!aptosWrappedAddress) {
throw new Error("Failed to create wrapped coin on Aptos");
}
const wrappedType = getAssetFullyQualifiedType(
aptosTokenBridge,
CHAIN_ID_ETH,
TEST_ERC20
);
if (!wrappedType) {
throw new Error("wrappedType is null");
}
const info = await getOriginalAssetAptos(
client,
aptosTokenBridge,
wrappedType
);
expect(uint8ArrayToHex(info.assetAddress)).toEqual(
tryNativeToHexString(TEST_ERC20, CHAIN_ID_ETH)
);
expect(info.chainId).toEqual(CHAIN_ID_ETH);
expect(info.isWrapped).toEqual(
await getIsWrappedAssetAptos(
client,
aptosTokenBridge,
aptosWrappedAddress
)
);
// transfer from eth
const balanceBeforeTransferEth = await getBalanceEth(TEST_ERC20, sender);
const amount = parseUnits("1", 18);
await approveEth(ethTokenBridge, TEST_ERC20, sender, amount);
const transferReceipt = await transferFromEth(
ethTokenBridge,
sender,
TEST_ERC20,
amount,
CHAIN_ID_APTOS,
tryNativeToUint8Array(recipient.address().hex(), CHAIN_ID_APTOS)
);
// get signed transfer vaa
sequence = parseSequenceFromLogEth(transferReceipt, ethCoreBridge);
expect(sequence).toBeTruthy();
const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_ETH,
getEmitterAddressEth(ethTokenBridge),
sequence,
{
transport: NodeHttpTransport(),
},
1000,
5
);
expect(transferVAA).toBeTruthy();
// register token on aptos
const script = registerCoin(aptosTokenBridge, CHAIN_ID_ETH, TEST_ERC20);
await signAndSubmitScript(client, recipient, script);
// redeem on aptos
const balanceBeforeTransferAptos = ethers.BigNumber.from(
await getBalanceAptos(client, wrappedType, recipient.address())
);
const redeemPayload = await redeemOnAptos(
client,
aptosTokenBridge,
transferVAA
);
await signAndSubmitEntryFunction(client, recipient, redeemPayload);
expect(
await getIsTransferCompletedAptos(client, aptosTokenBridge, transferVAA)
).toBe(true);
// check balances
const balanceAfterTransferAptos = ethers.BigNumber.from(
await getBalanceAptos(client, wrappedType, recipient.address())
);
expect(
balanceAfterTransferAptos.sub(balanceBeforeTransferAptos).toString()
).toEqual(parseUnits("1", 8).toString()); // max decimals is 8
const balanceAfterTransferEth = await getBalanceEth(TEST_ERC20, sender);
expect(
balanceBeforeTransferEth.sub(balanceAfterTransferEth).toString()
).toEqual(amount.toString());
// clean up
provider.destroy();
});
});
const getBalanceAptos = async (
client: AptosClient,
type: string,
address: HexString
): Promise<string> => {
const res = await client.getAccountResource(
address,
`0x1::coin::CoinStore<${type}>`
);
return (res.data as any).coin.value;
};
const getBalanceEth = (tokenAddress: string, wallet: ethers.Wallet) => {
let token = TokenImplementation__factory.connect(tokenAddress, wallet);
return token.balanceOf(wallet.address);
};

View File

@ -85,6 +85,10 @@ export const TERRA_HOST =
export const NEAR_NODE_URL = ci ? "http://near:3030" : "http://localhost:3030";
export const APTOS_NODE_URL = ci ? "http://aptos:8080/v1" : "http://0.0.0.0:8080/v1"
export const APTOS_FAUCET_URL = ci ? "http://aptos:8081" : "http://0.0.0.0:8081"
export const APTOS_PRIVATE_KEY = "537c1f91e56891445b491068f519b705f8c0f1a1e66111816dd5d4aa85b8113d";
describe("consts should exist", () => {
it("has Solana test token", () => {
expect.assertions(1);

View File

@ -21,6 +21,7 @@ import { importTokenWasm } from "../solana/wasm";
import {
callFunctionNear,
hashAccount,
ChainId,
textToHexString,
textToUint8Array,
uint8ArrayToHex,
@ -32,6 +33,8 @@ import { isNativeDenomInjective, isNativeDenomXpla } from "../cosmwasm";
import { Provider } from "near-api-js/lib/providers";
import { FunctionCallOptions } from "near-api-js/lib/account";
import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
import { Types } from "aptos";
import { attestToken as attestTokenAptos } from "../aptos";
export async function attestFromEth(
tokenBridgeAddress: string,
@ -319,3 +322,13 @@ export async function attestNearFromNear(
gas: new BN("100000000000000"),
};
}
// TODO: do we want to pass in a single assetAddress (instead of tokenChain and tokenAddress) as
// with other APIs above and let user derive the wrapped asset address themselves?
export function attestFromAptos(
tokenBridgeAddress: string,
tokenChain: ChainId,
tokenAddress: string
): Types.EntryFunctionPayload {
return attestTokenAptos(tokenBridgeAddress, tokenChain, tokenAddress);
}

View File

@ -1,18 +1,23 @@
import { Connection, PublicKey, Transaction } from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import { Algodv2 } from "algosdk";
import { Types } from "aptos";
import BN from "bn.js";
import { ethers, Overrides } from "ethers";
import { fromUint8Array } from "js-base64";
import { TransactionSignerPair, _submitVAAAlgorand } from "../algorand";
import { TransactionSignerPair, _parseVAAAlgorand, _submitVAAAlgorand } from "../algorand";
import { Bridge__factory } from "../ethers-contracts";
import { ixFromRust } from "../solana";
import { importTokenWasm } from "../solana/wasm";
import { submitVAAOnInjective } from "./redeem";
import BN from "bn.js";
import { FunctionCallOptions } from "near-api-js/lib/account";
import { Provider } from "near-api-js/lib/providers";
import { callFunctionNear } from "../utils";
import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
import {
createWrappedCoin as createWrappedCoinAptos,
createWrappedCoinType as createWrappedCoinTypeAptos,
} from "../aptos";
export async function createWrappedOnEth(
tokenBridgeAddress: string,
@ -61,12 +66,7 @@ export async function createWrappedOnSolana(
): Promise<Transaction> {
const { create_wrapped_ix } = await importTokenWasm();
const ix = ixFromRust(
create_wrapped_ix(
tokenBridgeAddress,
bridgeAddress,
payerAddress,
signedVAA
)
create_wrapped_ix(tokenBridgeAddress, bridgeAddress, payerAddress, signedVAA)
);
const transaction = new Transaction().add(ix);
const { blockhash } = await connection.getRecentBlockhash();
@ -82,13 +82,7 @@ export async function createWrappedOnAlgorand(
senderAddr: string,
attestVAA: Uint8Array
): Promise<TransactionSignerPair[]> {
return await _submitVAAAlgorand(
client,
tokenBridgeId,
bridgeId,
attestVAA,
senderAddr
);
return await _submitVAAAlgorand(client, tokenBridgeId, bridgeId, attestVAA, senderAddr);
}
export async function createWrappedOnNear(
@ -114,3 +108,17 @@ export async function createWrappedOnNear(
msgs.push({ ...msgs[0] });
return msgs;
}
export function createWrappedTypeOnAptos(
tokenBridgeAddress: string,
signedVAA: Uint8Array
): Types.EntryFunctionPayload {
return createWrappedCoinTypeAptos(tokenBridgeAddress, signedVAA);
}
export function createWrappedOnAptos(
tokenBridgeAddress: string,
attestVAA: Uint8Array
): Types.EntryFunctionPayload {
return createWrappedCoinAptos(tokenBridgeAddress, attestVAA);
}

View File

@ -2,13 +2,10 @@ import { Connection, PublicKey } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts";
import { Algodv2 } from "algosdk";
import { AptosClient } from "aptos";
import { ethers } from "ethers";
import { fromUint8Array } from "js-base64";
import {
calcLogicSigAccount,
decodeLocalState,
hexToNativeAssetBigIntAlgorand,
} from "../algorand";
import { calcLogicSigAccount, decodeLocalState, hexToNativeAssetBigIntAlgorand } from "../algorand";
import { Bridge__factory } from "../ethers-contracts";
import { importTokenWasm } from "../solana/wasm";
import {
@ -17,6 +14,8 @@ import {
ChainName,
CHAIN_ID_ALGORAND,
coalesceChainId,
ensureHexPrefix,
getForeignAssetAddress,
} from "../utils";
import { Provider } from "near-api-js/lib/providers";
import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
@ -37,10 +36,7 @@ export async function getForeignAssetEth(
): Promise<string | null> {
const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
try {
return await tokenBridge.wrappedAsset(
coalesceChainId(originChain),
originAsset
);
return await tokenBridge.wrappedAsset(coalesceChainId(originChain), originAsset);
} catch (e) {
return null;
}
@ -53,15 +49,12 @@ export async function getForeignAssetTerra(
originAsset: Uint8Array
): Promise<string | null> {
try {
const result: { address: string } = await client.wasm.contractQuery(
tokenBridgeAddress,
{
const result: { address: string } = await client.wasm.contractQuery(tokenBridgeAddress, {
wrapped_registry: {
chain: coalesceChainId(originChain),
address: fromUint8Array(originAsset),
},
}
);
});
return result.address;
} catch (e) {
return null;
@ -149,9 +142,7 @@ export async function getForeignAssetSolana(
coalesceChainId(originChain)
);
const wrappedAddressPK = new PublicKey(wrappedAddress);
const wrappedAssetAccountInfo = await connection.getAccountInfo(
wrappedAddressPK
);
const wrappedAssetAccountInfo = await connection.getAccountInfo(wrappedAddressPK);
return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
}
@ -174,11 +165,7 @@ export async function getForeignAssetAlgorand(
if (!doesExist) {
return null;
}
let asset: Uint8Array = await decodeLocalState(
client,
tokenBridgeId,
lsa.address()
);
let asset: Uint8Array = await decodeLocalState(client, tokenBridgeId, lsa.address());
if (asset.length > 8) {
const tmp = Buffer.from(asset.slice(0, 8));
return tmp.readBigUInt64BE(0);
@ -203,3 +190,24 @@ export async function getForeignAssetNear(
);
return ret !== "" ? ret : null;
}
export async function getForeignAssetAptos(
client: AptosClient,
tokenBridgeAddress: string,
originChain: ChainId | ChainName,
originAddress: string
): Promise<string | null> {
const originChainId = coalesceChainId(originChain);
const assetAddress = getForeignAssetAddress(tokenBridgeAddress, originChainId, originAddress);
if (!assetAddress) {
return null;
}
try {
// check if asset exists and throw if it doesn't
await client.getAccountResource(assetAddress, `0x1::coin::CoinInfo<${ensureHexPrefix(assetAddress)}::coin::T>`);
return assetAddress;
} catch (e) {
return null;
}
}

View File

@ -2,11 +2,12 @@ import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts";
import { Connection, PublicKey } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { Algodv2, bigIntToBytes } from "algosdk";
import { AptosClient } from "aptos";
import axios from "axios";
import { ethers } from "ethers";
import { fromUint8Array } from "js-base64";
import { redeemOnTerra } from ".";
import { TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
import { ensureHexPrefix, TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
import {
BITS_PER_KEY,
calcLogicSigAccount,
@ -20,11 +21,12 @@ import { importCoreWasm } from "../solana/wasm";
import { safeBigIntToNumber } from "../utils/bigint";
import { Provider } from "near-api-js/lib/providers";
import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
import { State } from "../aptos/types";
export async function getIsTransferCompletedEth(
tokenBridgeAddress: string,
provider: ethers.Signer | ethers.providers.Provider,
signedVAA: Uint8Array
signedVAA: Uint8Array,
): Promise<boolean> {
const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
const signedVAAHash = await getSignedVAAHash(signedVAA);
@ -38,18 +40,16 @@ export async function getIsTransferCompletedTerra(
tokenBridgeAddress: string,
signedVAA: Uint8Array,
client: LCDClient,
gasPriceUrl: string
gasPriceUrl: string,
): Promise<boolean> {
const msg = await redeemOnTerra(
tokenBridgeAddress,
TERRA_REDEEMED_CHECK_WALLET_ADDRESS,
signedVAA
signedVAA,
);
// TODO: remove gasPriceUrl and just use the client's gas prices
const gasPrices = await axios.get(gasPriceUrl).then((result) => result.data);
const account = await client.auth.accountInfo(
TERRA_REDEEMED_CHECK_WALLET_ADDRESS
);
const account = await client.auth.accountInfo(TERRA_REDEEMED_CHECK_WALLET_ADDRESS);
try {
await client.tx.estimateFee(
[
@ -63,7 +63,7 @@ export async function getIsTransferCompletedTerra(
memo: "already redeemed calculation",
feeDenoms: ["uluna"],
gasPrices,
}
},
);
} catch (e: any) {
// redeemed if the VAA was already executed
@ -136,28 +136,22 @@ export async function getIsTransferCompletedXpla(
signedVAA: Uint8Array,
client: XplaLCDClient
): Promise<boolean> {
const result: { is_redeemed: boolean } = await client.wasm.contractQuery(
tokenBridgeAddress,
{
const result: { is_redeemed: boolean } = await client.wasm.contractQuery(tokenBridgeAddress, {
is_vaa_redeemed: {
vaa: fromUint8Array(signedVAA),
},
}
);
});
return result.is_redeemed;
}
export async function getIsTransferCompletedSolana(
tokenBridgeAddress: string,
signedVAA: Uint8Array,
connection: Connection
connection: Connection,
): Promise<boolean> {
const { claim_address } = await importCoreWasm();
const claimAddress = await claim_address(tokenBridgeAddress, signedVAA);
const claimInfo = await connection.getAccountInfo(
new PublicKey(claimAddress),
"confirmed"
);
const claimInfo = await connection.getAccountInfo(new PublicKey(claimAddress), "confirmed");
return !!claimInfo;
}
@ -175,7 +169,7 @@ async function checkBitsSet(
client: Algodv2,
appId: bigint,
addr: string,
seq: bigint
seq: bigint,
): Promise<boolean> {
let retval: boolean = false;
let appState: any[] = [];
@ -222,7 +216,7 @@ async function checkBitsSet(
export async function getIsTransferCompletedAlgorand(
client: Algodv2,
appId: bigint,
signedVAA: Uint8Array
signedVAA: Uint8Array,
): Promise<boolean> {
const parsedVAA = _parseVAAAlgorand(signedVAA);
const seq: bigint = parsedVAA.sequence;
@ -232,7 +226,7 @@ export async function getIsTransferCompletedAlgorand(
client,
appId,
seq / BigInt(MAX_BITS),
chainRaw + em
chainRaw + em,
);
if (!doesExist) {
return false;
@ -245,7 +239,7 @@ export async function getIsTransferCompletedAlgorand(
export async function getIsTransferCompletedNear(
provider: Provider,
tokenBridge: string,
signedVAA: Uint8Array
signedVAA: Uint8Array,
): Promise<boolean> {
const vaa = Buffer.from(signedVAA).toString("hex");
return (
@ -254,3 +248,30 @@ export async function getIsTransferCompletedNear(
})
)[1];
}
export async function getIsTransferCompletedAptos(
client: AptosClient,
tokenBridgeAddress: string,
signedVAA: Uint8Array,
): Promise<boolean> {
// get handle
tokenBridgeAddress = ensureHexPrefix(tokenBridgeAddress);
const state = (
await client.getAccountResource(tokenBridgeAddress, `${tokenBridgeAddress}::state::State`)
).data as State;
const handle = state.consumed_vaas.elems.handle;
// check if vaa hash is in consumed_vaas
const signedVAAHash = await getSignedVAAHash(signedVAA);
try {
// when accessing Set<T>, key is type T and value is 0
await client.getTableItem(handle, {
key_type: "vector<u8>",
value_type: "u8",
key: signedVAAHash,
});
return true;
} catch {
return false;
}
}

View File

@ -2,10 +2,11 @@ import { ChainGrpcWasmApi } from "@injectivelabs/sdk-ts";
import { Connection, PublicKey } from "@solana/web3.js";
import { LCDClient } from "@terra-money/terra.js";
import { Algodv2, getApplicationAddress } from "algosdk";
import { AptosClient } from "aptos";
import { ethers } from "ethers";
import { Bridge__factory } from "../ethers-contracts";
import { importTokenWasm } from "../solana/wasm";
import { CHAIN_ID_INJECTIVE, tryNativeToHexString } from "../utils";
import { CHAIN_ID_INJECTIVE, ensureHexPrefix, tryNativeToHexString } from "../utils";
import { safeBigIntToNumber } from "../utils/bigint";
import { getForeignAssetInjective } from "./getForeignAsset";
@ -114,3 +115,19 @@ export function getIsWrappedAssetNear(
): boolean {
return asset.endsWith("." + tokenBridge);
}
// TODO: do we need to check if token is registered in bridge?
export async function getIsWrappedAssetAptos(
client: AptosClient,
tokenBridgeAddress: string,
assetAddress: string,
): Promise<boolean> {
assetAddress = ensureHexPrefix(assetAddress);
try {
// get origin info from asset address
await client.getAccountResource(assetAddress, `${tokenBridgeAddress}::state::OriginInfo`);
return true;
} catch {
return false;
}
}

View File

@ -11,9 +11,11 @@ import { importTokenWasm } from "../solana/wasm";
import { buildNativeId } from "../terra";
import { canonicalAddress } from "../cosmos";
import {
assertChain,
ChainId,
ChainName,
CHAIN_ID_ALGORAND,
CHAIN_ID_APTOS,
CHAIN_ID_NEAR,
CHAIN_ID_INJECTIVE,
CHAIN_ID_SOLANA,
@ -25,6 +27,7 @@ import {
coalesceCosmWasmChainId,
tryHexToNativeAssetString,
callFunctionNear,
isValidAptosType,
} from "../utils";
import { safeBigIntToNumber } from "../utils/bigint";
import {
@ -34,6 +37,9 @@ import {
} from "./getIsWrappedAsset";
import { Provider } from "near-api-js/lib/providers";
import { LCDClient as XplaLCDClient } from "@xpla/xpla.js";
import { AptosClient } from "aptos";
import { OriginInfo } from "../aptos/types"
import { sha3_256 } from "js-sha3";;
// TODO: remove `as ChainId` and return number in next minor version as we can't ensure it will match our type definition
export interface WormholeWrappedInfo {
@ -284,7 +290,7 @@ export async function getOriginalAssetNear(
chainId: CHAIN_ID_NEAR,
assetAddress: new Uint8Array(),
};
retVal.isWrapped = await getIsWrappedAssetNear(tokenAccount, assetAccount);
retVal.isWrapped = getIsWrappedAssetNear(tokenAccount, assetAccount);
if (!retVal.isWrapped) {
retVal.assetAddress = assetAccount
? arrayify(sha256(Buffer.from(assetAccount)))
@ -306,3 +312,50 @@ export async function getOriginalAssetNear(
return retVal;
}
export async function getOriginalAssetAptos(
client: AptosClient,
tokenBridgeAddress: string,
fullyQualifiedType: string
): Promise<WormholeWrappedInfo> {
if (!isValidAptosType(fullyQualifiedType)) {
throw new Error("Need fully qualified address");
}
let originInfo: OriginInfo | undefined;
try {
originInfo = (
await client.getAccountResource(
fullyQualifiedType.split("::")[0],
`${tokenBridgeAddress}::state::OriginInfo`
)
).data as OriginInfo;
} catch {
return {
isWrapped: false,
chainId: CHAIN_ID_APTOS,
assetAddress: hexToUint8Array(sha3_256(fullyQualifiedType)),
};
}
if (!!originInfo) {
// wrapped asset
const chainId = parseInt(originInfo.token_chain.number);
assertChain(chainId);
const assetAddress = hexToUint8Array(
originInfo.token_address.external_address.substring(2)
);
return {
isWrapped: true,
chainId,
assetAddress,
};
} else {
// native asset
return {
isWrapped: false,
chainId: CHAIN_ID_APTOS,
assetAddress: hexToUint8Array(sha3_256(fullyQualifiedType)),
};
}
}

View File

@ -23,7 +23,7 @@ import {
WSOL_DECIMALS,
uint8ArrayToHex,
callFunctionNear,
hashLookup,
hashLookup
} from "../utils";
import { getForeignAssetNear } from ".";
@ -37,6 +37,8 @@ import { MsgExecuteContract as MsgExecuteContractInjective } from "@injectivelab
import { FunctionCallOptions } from "near-api-js/lib/account";
import { Provider } from "near-api-js/lib/providers";
import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
import { AptosClient, Types } from "aptos";
import { completeTransfer as completeTransferAptos, completeTransferAndRegister } from "../aptos";
export async function redeemOnEth(
tokenBridgeAddress: string,
@ -382,3 +384,15 @@ export async function redeemOnNear(
return options;
}
export function redeemOnAptos(
client: AptosClient,
tokenBridgeAddress: string,
transferVAA: Uint8Array
): Promise<Types.EntryFunctionPayload> {
return completeTransferAndRegister(
client,
tokenBridgeAddress,
transferVAA
);
}

View File

@ -48,10 +48,12 @@ import {
} from "../utils";
import { safeBigIntToNumber } from "../utils/bigint";
import { isNativeDenomInjective, isNativeDenomXpla } from "../cosmwasm";
import { Types } from "aptos";
const BN = require("bn.js");
import { FunctionCallOptions } from "near-api-js/lib/account";
import { Provider } from "near-api-js/lib/providers";
import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
import { transferTokens as transferTokensAptos } from "../aptos";
export async function getAllowanceEth(
tokenBridgeAddress: string,
@ -958,3 +960,24 @@ export async function transferNearFromNear(
gas: new BN("100000000000000"),
};
}
export function transferFromAptos(
tokenBridgeAddress: string,
fullyQualifiedType: string,
amount: string,
recipientChain: ChainId | ChainName,
recipient: Uint8Array,
relayerFee: string = "0",
payload: string = ""
): Types.EntryFunctionPayload {
return transferTokensAptos(
tokenBridgeAddress,
fullyQualifiedType,
amount,
recipientChain,
recipient,
relayerFee,
createNonce().readUInt32LE(0),
payload
);
}

View File

@ -6,6 +6,7 @@ import {
createWrappedOnNear,
submitVAAOnInjective,
createWrappedOnXpla,
createWrappedOnAptos,
} from ".";
import { Bridge__factory } from "../ethers-contracts";
@ -32,3 +33,5 @@ export const updateWrappedOnSolana = createWrappedOnSolana;
export const updateWrappedOnAlgorand = createWrappedOnAlgorand;
export const updateWrappedOnNear = createWrappedOnNear;
export const updateWrappedOnAptos = createWrappedOnAptos;

195
sdk/js/src/utils/aptos.ts Normal file
View File

@ -0,0 +1,195 @@
import { hexZeroPad } from "ethers/lib/utils";
import { sha3_256 } from "js-sha3";
import { ChainId, CHAIN_ID_APTOS, ensureHexPrefix, hex } from "../utils";
import { AptosAccount, AptosClient, TxnBuilderTypes, Types } from "aptos";
import { State } from "../aptos/types";
export const signAndSubmitEntryFunction = (
client: AptosClient,
sender: AptosAccount,
payload: Types.EntryFunctionPayload,
opts?: Partial<Types.SubmitTransactionRequest>
): Promise<Types.UserTransaction> => {
// overwriting `max_gas_amount` and `gas_unit_price` defaults
// rest of defaults are defined here: https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#generateTransaction
const customOpts = Object.assign(
{
gas_unit_price: "100",
max_gas_amount: "30000",
},
opts
);
return client
.generateTransaction(sender.address(), payload, customOpts)
.then(
(rawTx) =>
signAndSubmitTransaction(
client,
sender,
rawTx
) as Promise<Types.UserTransaction>
);
};
export const signAndSubmitScript = async (
client: AptosClient,
sender: AptosAccount,
payload: TxnBuilderTypes.TransactionPayloadScript,
opts?: Partial<Types.SubmitTransactionRequest>
) => {
// overwriting `max_gas_amount` and `gas_unit_price` defaults
// rest of defaults are defined here: https://aptos-labs.github.io/ts-sdk-doc/classes/AptosClient.html#generateTransaction
const customOpts = Object.assign(
{
gas_unit_price: "100",
max_gas_amount: "30000",
},
opts
);
// create raw transaction
const [{ sequence_number: sequenceNumber }, chainId] = await Promise.all([
client.getAccount(sender.address()),
client.getChainId(),
]);
const rawTx = new TxnBuilderTypes.RawTransaction(
TxnBuilderTypes.AccountAddress.fromHex(sender.address()),
BigInt(sequenceNumber),
payload,
BigInt(customOpts.max_gas_amount),
BigInt(customOpts.gas_unit_price),
BigInt(Math.floor(Date.now() / 1000) + 10),
new TxnBuilderTypes.ChainId(chainId)
);
// sign & submit transaction
return signAndSubmitTransaction(client, sender, rawTx);
};
const signAndSubmitTransaction = async (
client: AptosClient,
sender: AptosAccount,
rawTx: TxnBuilderTypes.RawTransaction
): Promise<Types.Transaction> => {
// simulate transaction
await client.simulateTransaction(sender, rawTx).then((sims) =>
sims.forEach((tx) => {
if (!tx.success) {
throw new Error(
`Transaction failed: ${tx.vm_status}\n${JSON.stringify(tx, null, 2)}`
);
}
})
);
// sign & submit transaction
return client
.signTransaction(sender, rawTx)
.then((signedTx) => client.submitTransaction(signedTx))
.then((pendingTx) => client.waitForTransactionWithResult(pendingTx.hash));
};
export const getAssetFullyQualifiedType = (
tokenBridgeAddress: string, // 32 bytes
originChain: ChainId,
originAddress: string
): string | null => {
// native asset
if (originChain === CHAIN_ID_APTOS) {
// originAddress should be of form address::module::type
if (!isValidAptosType(originAddress)) {
console.error("Need fully qualified address for native asset");
return null;
}
return ensureHexPrefix(originAddress);
}
// non-native asset, derive unique address
const wrappedAssetAddress = getForeignAssetAddress(
tokenBridgeAddress,
originChain,
originAddress
);
return wrappedAssetAddress
? `${ensureHexPrefix(wrappedAssetAddress)}::coin::T`
: null;
};
export const getForeignAssetAddress = (
tokenBridgeAddress: string, // 32 bytes
originChain: ChainId,
originAddress: string
): string | null => {
if (originChain === CHAIN_ID_APTOS) {
return null;
}
// from https://github.com/aptos-labs/aptos-core/blob/25696fd266498d81d346fe86e01c330705a71465/aptos-move/framework/aptos-framework/sources/account.move#L90-L95
let DERIVE_RESOURCE_ACCOUNT_SCHEME = Buffer.alloc(1);
DERIVE_RESOURCE_ACCOUNT_SCHEME.writeUInt8(255);
let chain: Buffer = Buffer.alloc(2);
chain.writeUInt16BE(originChain);
return sha3_256(
Buffer.concat([
hex(hexZeroPad(ensureHexPrefix(tokenBridgeAddress), 32)),
chain,
Buffer.from("::", "ascii"),
hex(hexZeroPad(ensureHexPrefix(originAddress), 32)),
DERIVE_RESOURCE_ACCOUNT_SCHEME,
])
);
};
export const isValidAptosType = (address: string): boolean =>
/^(0x)?[0-9a-fA-F]+::\w+::\w+$/.test(address);
export const getExternalAddressFromType = (
fullyQualifiedType: string
): string => {
// hash the type so it fits into 32 bytes
return sha3_256(fullyQualifiedType);
};
export async function getTypeFromExternalAddress(
client: AptosClient,
tokenBridgeAddress: string,
fullyQualifiedTypeHash: string
): Promise<string | null> {
// get handle
tokenBridgeAddress = ensureHexPrefix(tokenBridgeAddress);
const state = (
await client.getAccountResource(
tokenBridgeAddress,
`${tokenBridgeAddress}::state::State`
)
).data as State;
const handle = state.native_infos.handle;
try {
// get type info
const typeInfo = await client.getTableItem(handle, {
key_type: `${tokenBridgeAddress}::token_hash::TokenHash`,
value_type: "0x1::type_info::TypeInfo",
key: { hash: fullyQualifiedTypeHash },
});
if (!typeInfo) {
return null;
}
// construct type
const moduleName = Buffer.from(
typeInfo.module_name.substring(2),
"hex"
).toString("ascii");
const structName = Buffer.from(
typeInfo.struct_name.substring(2),
"hex"
).toString("ascii");
return `${typeInfo.account_address}::${moduleName}::${structName}`;
} catch {
return null;
}
}

View File

@ -2,6 +2,7 @@ import { arrayify, zeroPad } from "@ethersproject/bytes";
import { PublicKey } from "@solana/web3.js";
import { hexValue, hexZeroPad, sha256, stripZeros } from "ethers/lib/utils";
import { Provider as NearProvider } from "near-api-js/lib/providers";
import { ethers } from "ethers";
import {
hexToNativeAssetStringAlgorand,
nativeStringToHexAlgorand,
@ -14,12 +15,13 @@ import {
ChainId,
ChainName,
CHAIN_ID_ALGORAND,
CHAIN_ID_NEAR,
CHAIN_ID_INJECTIVE,
CHAIN_ID_OSMOSIS,
CHAIN_ID_SUI,
CHAIN_ID_APTOS,
CHAIN_ID_INJECTIVE,
CHAIN_ID_NEAR,
CHAIN_ID_OSMOSIS,
CHAIN_ID_PYTHNET,
CHAIN_ID_SOLANA,
CHAIN_ID_SUI,
CHAIN_ID_TERRA,
CHAIN_ID_TERRA2,
CHAIN_ID_WORMCHAIN,
@ -27,10 +29,10 @@ import {
coalesceChainId,
isEVMChain,
isTerraChain,
CHAIN_ID_PYTHNET,
CHAIN_ID_XPLA,
} from "./consts";
import { hashLookup } from "./near";
import { getExternalAddressFromType, isValidAptosType } from "./aptos";
/**
*
@ -241,7 +243,11 @@ export const tryNativeToHexString = (
} else if (chainId === CHAIN_ID_SUI) {
throw Error("hexToNativeString: Sui not supported yet.");
} else if (chainId === CHAIN_ID_APTOS) {
throw Error("hexToNativeString: Aptos not supported yet.");
if (isValidAptosType(address)) {
return getExternalAddressFromType(address);
}
return uint8ArrayToHex(zeroPad(arrayify(address, { allowMissingPrefix:true }), 32));
} else if (chainId === CHAIN_ID_UNSET) {
throw Error("hexToNativeString: Chain id unset");
} else {
@ -309,3 +315,14 @@ export function textToHexString(name: string): string {
export function textToUint8Array(name: string): Uint8Array {
return new Uint8Array(Buffer.from(name, "binary"));
}
export function hex(x: string): Buffer {
return Buffer.from(
ethers.utils.hexlify(x, { allowMissingPrefix: true }).substring(2),
"hex"
);
}
export function ensureHexPrefix(x: string): string {
return x.substring(0, 2) !== "0x" ? `0x${x}` : x;
}

View File

@ -56,6 +56,7 @@ export type EVMChainName =
| "optimism"
| "gnosis"
| "ropsten";
/**
*
* All the Solana-based chain names that Wormhole supports
@ -75,6 +76,8 @@ export type ChainContracts = {
[chain in ChainName]: Contracts;
};
export type Network = "MAINNET" | "TESTNET" | "DEVNET";
const MAINNET = {
unset: {
core: undefined,
@ -167,8 +170,9 @@ const MAINNET = {
nft_bridge: undefined,
},
aptos: {
core: undefined,
token_bridge: undefined,
core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
token_bridge:
"0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
nft_bridge: undefined,
},
sui: {
@ -214,7 +218,8 @@ const MAINNET = {
},
xpla: {
core: "xpla1jn8qmdda5m6f6fqu9qv46rt7ajhklg40ukpqchkejcvy8x7w26cqxamv3w",
token_bridge: "xpla137w0wfch2dfmz7jl2ap8pcmswasj8kg06ay4dtjzw7tzkn77ufxqfw7acv",
token_bridge:
"xpla137w0wfch2dfmz7jl2ap8pcmswasj8kg06ay4dtjzw7tzkn77ufxqfw7acv",
nft_bridge: undefined,
},
ropsten: {
@ -321,8 +326,9 @@ const TESTNET = {
nft_bridge: undefined,
},
aptos: {
core: undefined,
token_bridge: undefined,
core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
token_bridge:
"0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
nft_bridge: undefined,
},
sui: {
@ -476,8 +482,9 @@ const DEVNET = {
nft_bridge: undefined,
},
aptos: {
core: undefined,
token_bridge: undefined,
core: "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017",
token_bridge:
"0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31",
nft_bridge: undefined,
},
sui: {
@ -781,6 +788,8 @@ export function assertEVMChain(
export const WSOL_ADDRESS = "So11111111111111111111111111111111111111112";
export const WSOL_DECIMALS = 9;
export const MAX_VAA_DECIMALS = 8;
export const APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS =
"0000000000000000000000000000000000000000000000000000000000000001";
export const TERRA_REDEEMED_CHECK_WALLET_ADDRESS =
"terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v";

View File

@ -1,6 +1,7 @@
export * from "./consts";
export * from "./createNonce";
export * from "./parseVaa";
export * from "./aptos";
export * from "./array";
export * from "./bigint";
export * from "./consts";
export * from "./createNonce";
export * from "./near";
export * from "./parseVaa";