Refactor SDK (#68)

Add EVM Testnet and Solana Devnet Integration Tests
This commit is contained in:
Karl 2022-08-16 09:54:02 -05:00 committed by GitHub
parent b4122bf0f0
commit 89f44e8f75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
120 changed files with 12697 additions and 48744 deletions

1
.envrc
View File

@ -1 +0,0 @@
eval "$(lorri direnv)"

View File

@ -1,13 +0,0 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="guardian-0 logs" type="ShConfigurationType">
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/scripts/tail.sh" />
<option name="SCRIPT_OPTIONS" value="guardian-0" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="/bin/bash" />
<option name="INTERPRETER_OPTIONS" value="" />
<method v="2" />
</configuration>
</component>

View File

@ -1,7 +0,0 @@
githubRepoOwner: certusone
githubRepoName: wormhole
githubHost: github.com
requireChecks: true
requireApproval: true
githubRemote: origin
githubBranch: dev.v2

View File

@ -10,7 +10,6 @@ clean:
cd ethereum && make clean
cd terra && make clean
cd sdk/js && rm -rf node_modules contracts lib src/icco/__tests__/tilt.json
cd tools && rm -rf node_modules lib
rm -f tilt.json
.PHONY: ethereum
@ -60,4 +59,4 @@ tilt-test: sdk sdk/js/src/icco/__tests__/tilt.json
cd sdk/js && npm run build && npm run test
sdk/js/src/icco/__tests__/tilt.json:
cp tilt.json sdk/js/src/icco/__tests__/tilt.json
cp tilt.json sdk/js/src/icco/__tests__/tilt.json

View File

@ -1,7 +1,10 @@
[features]
seeds = false
[programs.localnet]
anchor_contributor = "Efzc4SLs1ZdTPRq95oWxdMUr9XiX5M14HABwHpvrc9Fm"
anchor_contributor = "NEXaa1zDNLJ9AqwEd7LipQTge4ygeVVHyr8Tv7X2FCn"
[programs.devnet]
anchor_contributor = "NEXaa1zDNLJ9AqwEd7LipQTge4ygeVVHyr8Tv7X2FCn"
[registry]
url = "https://anchor.projectserum.com"

View File

@ -1,5 +1,5 @@
export CONDUCTOR_CHAIN=2
export CONDUCTOR_ADDRESS="000000000000000000000000ce121ea9c289390df7d812f83ed6be79a167dfe4"
export CONDUCTOR_CHAIN=6
export CONDUCTOR_ADDRESS="000000000000000000000000e9b4337f3ec72c6eaa519475e54cb2ba7621a7e0"
export CORE_BRIDGE_ADDRESS="3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"
export TOKEN_BRIDGE_ADDRESS="DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe"

View File

@ -0,0 +1,44 @@
import { postVaaSolanaWithRetry } from "@certusone/wormhole-sdk";
import { web3 } from "@project-serum/anchor";
import { IccoContributor } from "../tests/helpers/contributor";
import { connectToContributorProgram, readJson, readKeypair } from "./utils";
const CORE_BRIDGE_ADDRESS = new web3.PublicKey(process.env.CORE_BRIDGE_ADDRESS);
const TOKEN_BRIDGE_ADDRESS = new web3.PublicKey(process.env.TOKEN_BRIDGE_ADDRESS);
async function main() {
const rpc = "https://api.devnet.solana.com";
const contributorIdl = readJson(`${__dirname}/../target/idl/anchor_contributor.json`);
const programId = readKeypair(`${__dirname}/../target/deploy/anchor_contributor-keypair.json`).publicKey;
const payer = readKeypair(process.env.WALLET);
console.log("wormhole", CORE_BRIDGE_ADDRESS.toString());
console.log("token bridge", TOKEN_BRIDGE_ADDRESS.toString());
console.log("program id", programId.toString());
console.log("payer", payer.publicKey.toString());
const program = connectToContributorProgram(rpc, contributorIdl, programId, payer);
const connection = program.provider.connection;
const contributor = new IccoContributor(program, CORE_BRIDGE_ADDRESS, TOKEN_BRIDGE_ADDRESS, postVaaSolanaWithRetry);
try {
const tx = await contributor.createCustodian(payer);
console.log("tx", tx);
} catch (e) {
const custodianAccountInfo = await connection
.getAccountInfo(contributor.deriveCustodianAccount())
.catch((_) => null);
if (custodianAccountInfo == null) {
throw e;
} else {
console.log("custodian already created");
}
}
const custodian = contributor.deriveCustodianAccount();
console.log("custodian", custodian.toString());
}
main();

View File

@ -7,5 +7,13 @@ solana config set --url devnet
# WALLET must be set
ls $WALLET
# and PROGRAM_ID
ls $PROGRAM_KEY
. devnet.env
cp -i $PROGRAM_KEY target/deploy/anchor_contributor-keypair.json
anchor build --provider.cluster devnet
solana program deploy target/deploy/anchor_contributor.so -k $WALLET
# create custodian if it doesn't exist
ts-node migrations/create-devnet-custodian.ts

View File

@ -0,0 +1,29 @@
import { AnchorProvider, Program, web3 } from "@project-serum/anchor";
import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
import fs from "fs";
import { AnchorContributor } from "../target/types/anchor_contributor";
export function readJson(filename: string): any {
if (!fs.existsSync(filename)) {
throw Error(`${filename} does not exist`);
}
return JSON.parse(fs.readFileSync(filename, "utf8"));
}
export function readKeypair(filename: string): web3.Keypair {
return web3.Keypair.fromSecretKey(Uint8Array.from(readJson(filename)));
}
export function connectToContributorProgram(
rpc: string,
contributorIdl: any,
programId: web3.PublicKey,
wallet: web3.Keypair
): Program<AnchorContributor> {
const program = new Program<AnchorContributor>(
contributorIdl as AnchorContributor,
programId,
new AnchorProvider(new web3.Connection(rpc), new NodeWallet(wallet), {})
);
return program;
}

View File

@ -15,7 +15,7 @@ use error::*;
use token_bridge::*;
use wormhole::*;
declare_id!("Efzc4SLs1ZdTPRq95oWxdMUr9XiX5M14HABwHpvrc9Fm");
declare_id!("NEXaa1zDNLJ9AqwEd7LipQTge4ygeVVHyr8Tv7X2FCn");
#[program]
pub mod anchor_contributor {

View File

@ -563,7 +563,7 @@ function parseSaleId(iccoVaa: Buffer): Buffer {
return getVaaBody(iccoVaa).subarray(1, 33);
}
function hashVaaPayload(signedVaa: Buffer): Buffer {
export function hashVaaPayload(signedVaa: Buffer): Buffer {
const sigStart = 6;
const numSigners = signedVaa[5];
const sigLength = 66;

View File

@ -25,6 +25,13 @@
protobufjs "^6.11.2"
rxjs "^7.3.0"
"@cspotcode/source-map-support@^0.8.0":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
dependencies:
"@jridgewell/trace-mapping" "0.3.9"
"@ethersproject/abi@5.6.3", "@ethersproject/abi@^5.6.3":
version "5.6.3"
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.6.3.tgz#2d643544abadf6e6b63150508af43475985c23db"
@ -394,6 +401,24 @@
dependencies:
browser-headers "^0.4.1"
"@jridgewell/resolve-uri@^3.0.3":
version "3.0.7"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz#30cd49820a962aff48c8fffc5cd760151fca61fe"
integrity sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==
"@jridgewell/sourcemap-codec@^1.4.10":
version "1.4.13"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz#b6461fb0c2964356c469e115f504c95ad97ab88c"
integrity sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==
"@jridgewell/trace-mapping@0.3.9":
version "0.3.9"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
dependencies:
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@project-serum/anchor@^0.24.2":
version "0.24.2"
resolved "https://registry.yarnpkg.com/@project-serum/anchor/-/anchor-0.24.2.tgz#a3c52a99605c80735f446ca9b3a4885034731004"
@ -581,6 +606,26 @@
long "^4.0.0"
protobufjs "~6.11.2"
"@tsconfig/node10@^1.0.7":
version "1.0.9"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
"@tsconfig/node12@^1.0.7":
version "1.0.11"
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
"@tsconfig/node14@^1.0.0":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
"@tsconfig/node16@^1.0.2":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
"@types/bn.js@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/bn.js/-/bn.js-5.1.0.tgz#32c5d271503a12653c62cf4d2b45e6eab8cebc68"
@ -693,6 +738,16 @@ JSONStream@^1.3.5:
jsonparse "^1.2.0"
through ">=2.2.7 <3"
acorn-walk@^8.1.1:
version "8.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
acorn@^8.4.1:
version "8.7.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.1.tgz#0197122c843d1bf6d0a5e83220a788f278f63c30"
integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==
aes-js@3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d"
@ -744,6 +799,11 @@ anymatch@~3.1.2:
normalize-path "^3.0.0"
picomatch "^2.0.4"
arg@^4.1.0:
version "4.1.3"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
@ -1142,6 +1202,11 @@ create-hmac@^1.1.4, create-hmac@^1.1.7:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
create-require@^1.1.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
cross-fetch@^3.1.4, cross-fetch@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
@ -1221,6 +1286,11 @@ diff@^3.1.0:
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==
diff@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
dot-case@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"
@ -2494,6 +2564,25 @@ ts-node@7.0.1:
source-map-support "^0.5.6"
yn "^2.0.0"
ts-node@^10.8.1:
version "10.8.1"
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.8.1.tgz#ea2bd3459011b52699d7e88daa55a45a1af4f066"
integrity sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==
dependencies:
"@cspotcode/source-map-support" "^0.8.0"
"@tsconfig/node10" "^1.0.7"
"@tsconfig/node12" "^1.0.7"
"@tsconfig/node14" "^1.0.0"
"@tsconfig/node16" "^1.0.2"
acorn "^8.4.1"
acorn-walk "^8.1.1"
arg "^4.1.0"
create-require "^1.1.0"
diff "^4.0.1"
make-error "^1.1.1"
v8-compile-cache-lib "^3.0.1"
yn "3.1.1"
tsconfig-paths@^3.5.0:
version "3.14.1"
resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.1.tgz#ba0734599e8ea36c862798e920bcf163277b137a"
@ -2559,6 +2648,11 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
wait-on@6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-6.0.0.tgz#7e9bf8e3d7fe2daecbb7a570ac8ca41e9311c7e7"
@ -2687,6 +2781,11 @@ yargs@16.2.0:
y18n "^5.0.5"
yargs-parser "^20.2.2"
yn@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
yn@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a"

View File

@ -1,6 +0,0 @@
#!/bin/bash
cd migrations/
npx truffle migrate --f 2 --to 2 --network goerli --skip-dry-run
npx truffle-migrate --f 3 --to 3 --network goerli --skip-dry-run
npx truffle-migrate --f 3 --to 3 --network fuji --skip-dry-run

View File

@ -64,7 +64,7 @@ module.exports = async function(deployer, network) {
}
// testnet deployments
if (network == "goerli") {
if (network == "goerli" || network == "fuji") {
const fp = `${ethereumRootPath}/../testnet.json`;
const contents = fs.existsSync(fp) ? JSON.parse(fs.readFileSync(fp, "utf8")) : {};

7
sdk/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
node_modules
evm-contracts
target
src/ethers-contracts
src/anchor
cfg/testnet/contributors.json
cfg/testnet/saleConfig.json

View File

@ -1,18 +1,21 @@
{
"conductorNetwork": "goerli",
"conductorNetwork": "fuji",
"authority": "ADD AUTHORITY PRIVATE KEY",
"denominationDecimals": 6,
"raiseParams": {
"isFixedPrice": true,
"token": "uusd",
"localTokenAddress": "0x36Ed51Afc79619b299b238898E72ce482600568a",
"tokenChain": 3,
"tokenAmount": "1",
"minRaise": "0.01",
"maxRaise": ".10",
"minRaise": "5",
"maxRaise": "40",
"recipient": "ADD RECIPIENT PUBLIC KEY",
"refundRecipient": "ADD REFUND RECIPIENT PUBLIC KEY",
"saleDurationSeconds": 120,
"saleStartTimer": 90
"saleDurationSeconds": 200,
"lockUpDurationSeconds": 40,
"saleStartTimer": 50,
"authority": "ADD KYC AUTHORITY PUBLIC KEY"
},
"acceptedTokens": [
{

33
sdk/js/.gitignore vendored
View File

@ -1,33 +0,0 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# ethereum contracts
/contracts
/src/ethers-contracts
# tsproto output
/src/proto
# build
/lib

View File

@ -1,165 +0,0 @@
# Changelog
## 0.2.0
### Changed
Updated @terra-money/terra.js to 3.0.7
Removed @terra-money/wallet-provider
Removed walletAddress parameter from getIsTransferCompletedTerra
## 0.1.7
### Added
Fantom support
Aurora support
## 0.1.6
### Added
added parseSequencesFromLog\*
Terra NFT token bridge
getIsTransferCompleted on NFT bridge
export for wasm, createPostVaaInstructionSolana, createVerifySignaturesInstructionsSolana, postVaaSolana, postVaaSolanaWithRetry, and getSignedVAAWithRetry
re-export top level objects ethers_contracts, solana, terra, rpc, utils, bridge, token_bridge, nft_bridge
## 0.1.5
### Added
added postVaaSolanaWithRetry, which will retry transactions which failed during processing
added createVerifySignaturesInstructions, createPostVaaInstruction, which allows users to construct the postVaa process for themselves at the instruction level
added chunks and sendAndConfirmTransactionsWithRetry as utility functions
added integration tests for postVaaSolanaWithRetry
initial Oasis support
### Changed
deprecated postVaaSolana
## 0.1.4
initial AVAX testnet support
## 0.1.3
### Added
getSignedVAAHash
getIsTransferCompleted
## 0.1.1
### Added
CHAIN_ID_ETHEREUM_ROPSTEN
## 0.1.0
### Added
separate cjs and esm builds
updateWrappedOnSolana
top-level export getSignedVAAWithRetry
## 0.0.10
### Added
uint8ArrayToNative utility function for converting to native addresses from the uint8 format
Include node target wasms in lib
## 0.0.9
### Added
Integration tests
NodeJS target wasm
Ability to update attestations on EVM chains & Terra.
nativeToHexString utility function for converting native addresses into VAA hex format.
## 0.0.8
### Added
Polygon ChainId
## 0.0.7
### Changed
Changed function signature of attestFromTerra to be consistent with other terra functions
Removed hardcoded fees on terra transactions
## 0.0.6
### Changed
Allow separate payer and owner for Solana transfers
Support multiple EVM chains
Support native Terra tokens
Fixed nft_bridge getForeignAssetEth
## 0.0.5
### Added
NFT Bridge Support
getClaimAddressSolana
createMetaOnSolana
## 0.0.4
### Added
redeemOnEthNative
transferFromEthNative
## 0.0.3
### Added
Migration
NFT Bridge
### Changed
Fixed number overflow
Fixed guardian set index
## 0.0.2
Fix move postinstall to build
## 0.0.1
Initial release

View File

@ -1,208 +0,0 @@
# Wormhole SDK
> Note: This is a pre-alpha release and in active development. Function names and signatures are subject to change.
## What is Wormhole?
[Wormhole](https://wormholenetwork.com/) allows for the transmission of arbitrary data across multiple blockchains. Wormhole currently supports the following platforms:
- Solana
- Ethereum
- Terra
- Binance Smart Chain
Wormhole is, at its base layer, a very simple protocol. A Wormhole smart contract has been deployed on each of the supported blockchains, and users can emit messages in the Wormhole Network by submitting data to the smart contracts. These messages are quite simple and only have the following six fields.
- _emitterChain_ - The blockchain from which this message originates.
- _emitterAddress_ - The public address which submitted the message.
- _consistencyLevel_ - The number of blocks / slots which should pass before this message is considered confirmed.
- _timestamp_ - The timestamp when the Wormhole Network confirmed the message.
- _sequence_ - An incrementing sequence which denotes how many messages this _emitterAddress_ has emitted.
- _payload_ - The arbitrary contents of this message.
Whenever a wormhole contract processes one of these messages, participants in the Wormhole Network ( individually known as **Guardians** ), will observe the transaction and create a **SignedVAA** (Signed Verifiable Action Approval) once the transaction has reached the specified confirmation time on the emitter chain.
The SignedVAA is essentially an affirmation from the Wormhole Network that a transaction has been finalized on the emitter chain, and that any dependent actions on other chains may proceed.
While simple, the Wormhole Protocol provides a powerful base layer upon which many 'bridge' applications can be built. Because Wormhole is capable of verifying arbitrary data, bridges utilizing it are able to transfer native currencies, tokens, NFTs, oracle data, governance votes, and a whole host of other forms of decentralized data.
## How the Core Wormhole Bridge Works
The core Wormhole bridge operates by running smart contracts on both the _Source Chain_ (where the data currently resides) and the _Target Chain_ (where the data will be moved), and generally follows this workflow:
1) An end user or another smart contract publishes a message using the Bridge Contract on the Source Chain.
2) The Wormhole Network observes this transaction and issues a SignedVAA once it crosses its confirmation threshold.
3) An off-chain process collects the SignedVAA and submits it in a transaction to the Bridge Contract on the Target Chain, which can parse and verify the message.
## How the Wormhole Token Bridge Works
It is important to note that the Wormhole Token Bridge is not, strictly speaking, part of the Wormhole protocol, but rather a bridge on top of it. However, as token transfers are such an important use-case of the bridge, it is built and packaged as part of the Wormhole SDK.
The Token Bridge works in the same fashion as above, leveraging the Core Bridge to publish messages. However, there are actually two different functions in the token bridge: Attest and Transfer.
### Attest
Attestation is the process by which a token is 'registered' with the token bridge. Before being transferred, tokens must first be attested on their **Origin Chain** and have corresponding wrapped tokens created on the **Foreign Chain** to which they will be transferred. Attesting on the Origin Chain will create requisite addresses and metadata that will allow the wrapped asset to exist on Foreign Chains.
### Transfer
Once attested, tokens are mapped from their Native Chain to 'wrapped' assets on the Foreign Chains. Transferring an Ethereum-native token to Solana will result in a 'wrapped asset' on Solana, and transferring that same asset back to Ethereum will restore the native token.
It is important to note that Wormhole wrapped tokens are distinct from and incompatible with tokens wrapped by other bridges. Transferring a token which was wrapped by a different bridge will not redeem the native token, but rather will result in a 'double-wrapped' token.
## Examples
### Attest
#### Solana to Ethereum
```js
// Submit transaction - results in a Wormhole message being published
const transaction = await attestFromSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
mintAddress
);
const signed = await wallet.signTransaction(transaction);
const txid = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction(txid);
// Get the sequence number and emitter address required to fetch the signedVAA of our message
const info = await connection.getTransaction(txid);
const sequence = parseSequenceFromLogSolana(info);
const emitterAddress = await getEmitterAddressSolana(SOL_TOKEN_BRIDGE_ADDRESS);
// Fetch the signedVAA from the Wormhole Network (this may require retries while you wait for confirmation)
const { signedVAA } = await getSignedVAA(
WORMHOLE_RPC_HOST,
CHAIN_ID_SOLANA,
emitterAddress,
sequence
);
// Create the wrapped token on Ethereum
await createWrappedOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
```
#### Ethereum to Solana
```js
// Submit transaction - results in a Wormhole message being published
const receipt = await attestFromEth(
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
tokenAddress
);
// Get the sequence number and emitter address required to fetch the signedVAA of our message
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
// Fetch the signedVAA from the Wormhole Network (this may require retries while you wait for confirmation)
const { signedVAA } = await getSignedVAA(
WORMHOLE_RPC_HOST,
CHAIN_ID_ETH,
emitterAddress,
sequence
);
// On Solana, we have to post the signedVAA ourselves
await postVaaSolana(
connection,
wallet,
SOL_BRIDGE_ADDRESS,
payerAddress,
signedVAA
);
// Finally, create the wrapped token
const transaction = await createWrappedOnSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
signedVAA
);
const signed = await wallet.signTransaction(transaction);
const txid = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction(txid);
```
### Transfer
#### Solana to Ethereum
```js
// Submit transaction - results in a Wormhole message being published
const transaction = await transferFromSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
fromAddress,
mintAddress,
amount,
targetAddress,
CHAIN_ID_ETH,
originAddress,
originChain
);
const signed = await wallet.signTransaction(transaction);
const txid = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction(txid);
// Get the sequence number and emitter address required to fetch the signedVAA of our message
const info = await connection.getTransaction(txid);
const sequence = parseSequenceFromLogSolana(info);
const emitterAddress = await getEmitterAddressSolana(SOL_TOKEN_BRIDGE_ADDRESS);
// Fetch the signedVAA from the Wormhole Network (this may require retries while you wait for confirmation)
const { signedVAA } = await getSignedVAA(
WORMHOLE_RPC_HOST,
CHAIN_ID_SOLANA,
emitterAddress,
sequence
);
// Redeem on Ethereum
await redeemOnEth(ETH_TOKEN_BRIDGE_ADDRESS, signer, signedVAA);
```
#### Ethereum to Solana
```js
// Submit transaction - results in a Wormhole message being published
const receipt = await transferFromEth(
ETH_TOKEN_BRIDGE_ADDRESS,
signer,
tokenAddress,
amount,
CHAIN_ID_SOLANA,
recipientAddress
);
// Get the sequence number and emitter address required to fetch the signedVAA of our message
const sequence = parseSequenceFromLogEth(receipt, ETH_BRIDGE_ADDRESS);
const emitterAddress = getEmitterAddressEth(ETH_TOKEN_BRIDGE_ADDRESS);
// Fetch the signedVAA from the Wormhole Network (this may require retries while you wait for confirmation)
const { signedVAA } = await getSignedVAA(
WORMHOLE_RPC_HOST,
CHAIN_ID_ETH,
emitterAddress,
sequence
);
// On Solana, we have to post the signedVAA ourselves
await postVaaSolana(
connection,
wallet,
SOL_BRIDGE_ADDRESS,
payerAddress,
signedVAA
);
// Finally, redeem on Solana
const transaction = await redeemOnSolana(
connection,
SOL_BRIDGE_ADDRESS,
SOL_TOKEN_BRIDGE_ADDRESS,
payerAddress,
signedVAA,
isSolanaNative,
mintAddress
);
const signed = await wallet.signTransaction(transaction);
const txid = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction(txid);
```

View File

@ -1,8 +0,0 @@
{
"transform": {
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
"testTimeout": 60000
}

20554
sdk/js/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,76 +0,0 @@
{
"name": "@certusone/wormhole-icco-sdk",
"version": "0.1.0",
"description": "SDK for interacting with ICCO",
"homepage": "https://wormholenetwork.com",
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"files": [
"lib/"
],
"repository": "https://github.com/certusone/wormhole-icco/tree/main/sdk/js",
"scripts": {
"build-contracts": "npm run build --prefix ../../ethereum && node scripts/copyContracts.js && typechain --target=ethers-v5 --out-dir=src/ethers-contracts contracts/*.json",
"build-abis": "typechain --target=ethers-v5 --out-dir=src/ethers-contracts/abi",
"build-deps": "npm run build-abis && npm run build-contracts",
"build-lib": "tsc -p tsconfig.json && tsc -p tsconfig-cjs.json && node scripts/copyEthersTypes.js",
"build-all": "npm run build-deps && npm run build-lib",
"test": "jest --config jestconfig.json --verbose --forceExit",
"test-ci": "jest --config jestconfig.json --verbose --setupFiles ./ci-config.js --forceExit",
"build": "npm run build-all",
"format": "echo \"disabled: prettier --write \"src/**/*.ts\"\"",
"lint": "tslint -p tsconfig.json",
"prepublishOnly": "echo \"disabled: npm test && npm run lint\"",
"preversion": "npm run lint",
"version": "npm run format && git add -A src",
"postversion": "git push && git push --tags"
},
"keywords": [
"wormhole",
"portal",
"icco",
"ico",
"ido",
"sdk",
"solana",
"ethereum",
"terra",
"bsc",
"polygon",
"avax",
"fantom",
"aurora"
],
"author": "certusone",
"license": "Apache-2.0",
"devDependencies": {
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@openzeppelin/contracts": "^4.2.0",
"@typechain/ethers-v5": "^7.0.1",
"@types/jest": "^27.0.2",
"@types/long": "^4.0.1",
"@types/node": "^16.6.1",
"@types/react": "^17.0.19",
"copy-dir": "^1.3.0",
"ethers": "^5.4.4",
"jest": "^27.3.1",
"prettier": "^2.3.2",
"ts-jest": "^27.0.7",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.3.5",
"web3": "^1.6.1"
},
"dependencies": {
"@certusone/wormhole-sdk": "^0.3.5",
"@improbable-eng/grpc-web": "^0.14.0",
"@solana/spl-token": "^0.1.8",
"@solana/web3.js": "^1.24.0",
"@terra-money/terra.js": "^3.0.7",
"axios": "^0.24.0",
"bech32": "^2.0.0",
"js-base64": "^3.6.1",
"protobufjs": "^6.11.2",
"rxjs": "^7.3.0"
}
}

View File

@ -1,2 +0,0 @@
const copydir = require("copy-dir");
copydir.sync("../../ethereum/build/contracts", "./contracts");

View File

@ -1,67 +0,0 @@
const fs = require("fs");
["lib/esm", "lib/cjs"].forEach((buildPath) => {
fs.copyFileSync(
`src/solana/core/bridge_bg.wasm`,
`${buildPath}/solana/core/bridge_bg.wasm`
);
fs.copyFileSync(
`src/solana/core-node/bridge_bg.wasm`,
`${buildPath}/solana/core-node/bridge_bg.wasm`
);
fs.copyFileSync(
`src/solana/core/bridge_bg.wasm.d.ts`,
`${buildPath}/solana/core/bridge_bg.wasm.d.ts`
);
fs.copyFileSync(
`src/solana/core-node/bridge_bg.wasm.d.ts`,
`${buildPath}/solana/core-node/bridge_bg.wasm.d.ts`
);
fs.copyFileSync(
`src/solana/nft/nft_bridge_bg.wasm`,
`${buildPath}/solana/nft/nft_bridge_bg.wasm`
);
fs.copyFileSync(
`src/solana/nft-node/nft_bridge_bg.wasm`,
`${buildPath}/solana/nft-node/nft_bridge_bg.wasm`
);
fs.copyFileSync(
`src/solana/nft/nft_bridge_bg.wasm.d.ts`,
`${buildPath}/solana/nft/nft_bridge_bg.wasm.d.ts`
);
fs.copyFileSync(
`src/solana/nft-node/nft_bridge_bg.wasm.d.ts`,
`${buildPath}/solana/nft-node/nft_bridge_bg.wasm.d.ts`
);
fs.copyFileSync(
`src/solana/token/token_bridge_bg.wasm`,
`${buildPath}/solana/token/token_bridge_bg.wasm`
);
fs.copyFileSync(
`src/solana/token-node/token_bridge_bg.wasm`,
`${buildPath}/solana/token-node/token_bridge_bg.wasm`
);
fs.copyFileSync(
`src/solana/token/token_bridge_bg.wasm.d.ts`,
`${buildPath}/solana/token/token_bridge_bg.wasm.d.ts`
);
fs.copyFileSync(
`src/solana/token-node/token_bridge_bg.wasm.d.ts`,
`${buildPath}/solana/token-node/token_bridge_bg.wasm.d.ts`
);
fs.copyFileSync(
`src/solana/migration/wormhole_migration_bg.wasm`,
`${buildPath}/solana/migration/wormhole_migration_bg.wasm`
);
fs.copyFileSync(
`src/solana/migration-node/wormhole_migration_bg.wasm`,
`${buildPath}/solana/migration-node/wormhole_migration_bg.wasm`
);
fs.copyFileSync(
`src/solana/migration/wormhole_migration_bg.wasm.d.ts`,
`${buildPath}/solana/migration/wormhole_migration_bg.wasm.d.ts`
);
fs.copyFileSync(
`src/solana/migration-node/wormhole_migration_bg.wasm.d.ts`,
`${buildPath}/solana/migration-node/wormhole_migration_bg.wasm.d.ts`
);
});

View File

@ -1,52 +0,0 @@
import { describe, expect, it } from "@jest/globals";
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_BSC } from "@certusone/wormhole-sdk";
import Tilt from "./tilt.json";
const ci = !!process.env.CI;
// see devnet.md
export const ETH_NODE_URL = ci ? "ws://eth-devnet:8545" : "ws://localhost:8545";
export const BSC_NODE_URL = ci ? "ws://eth-devnet:8546" : "ws://localhost:8546";
export const ETH_PRIVATE_KEY1 =
"0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c"; // account 2
export const ETH_PRIVATE_KEY2 =
"0x646f1ce2fdad0e6deeeb5c7e8e5543bdde65e86029e2fd9fc169899c440a7913"; // account 3
export const ETH_PRIVATE_KEY3 =
"0xadd53f9a7e588d003326d1cbf9e4a43c061aadd9bc938c843a79e7b4fd2ad743"; // account 4
export const ETH_PRIVATE_KEY4 =
"0x395df67f0c2d2d9fe1ad08d1bc8b6627011959b79c53d7dd6a3536a33ab8a4fd"; // account 5
export const ETH_PRIVATE_KEY5 =
"0xe485d098507f54e7733a205420dfddbe58db035fa577fc294ebd14db90767a52"; // account 6
export const KYC_PRIVATE_KEYS =
"0xb0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773"; // account 9
export const ETH_CORE_BRIDGE_ADDRESS =
"0xC89Ce4735882C9F0f0FE26686c53074E09B0D550";
export const ETH_TOKEN_BRIDGE_ADDRESS =
"0x0290FB167208Af455bB137780163b7B7a9a10C16";
// decimals for min/max raise denomination
export const DENOMINATION_DECIMALS = 18;
// contributors only registered with conductor on CHAIN_ID_ETH
export const ETH_TOKEN_SALE_CONDUCTOR_ADDRESS = Tilt.conductorAddress;
export const ETH_TOKEN_SALE_CONDUCTOR_CHAIN_ID = Tilt.conductorChain;
export const TOKEN_SALE_CONTRIBUTOR_ADDRESSES = new Map<ChainId, string>();
TOKEN_SALE_CONTRIBUTOR_ADDRESSES.set(CHAIN_ID_ETH, Tilt.ethContributorAddress);
TOKEN_SALE_CONTRIBUTOR_ADDRESSES.set(CHAIN_ID_BSC, Tilt.bscContributorAddress);
export const WETH_ADDRESS = "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E";
export const WBNB_ADDRESS = "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E";
export const WORMHOLE_RPC_HOSTS = ci
? ["http://guardian:7071"]
: ["http://localhost:7071"];
describe("consts should exist", () => {
it("Contributors Defined Is Correct Number", () => {
expect.assertions(1);
expect(TOKEN_SALE_CONTRIBUTOR_ADDRESSES.size).toEqual(2);
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,270 +0,0 @@
// Test vaas from Karl.
//first sale: successful
const initSaleVaa =
"0100000000010096e02874d784b992173e54b27a29cf8d4d599e2229c03946e29801efd94fa82f4ea593459c8dde6fdf97fce5f450c47744ba7780c5262a2e88406e0dc4b513ba01000001170000000000020000000000000000000000005f8e26facc23fa4cbd87b8d9dbbd33d5047abde100000000000000000f01000000000000000000000000000000000000000000000000000000000000000000000000000000000000000083752ecafebf4707258dedffbd9c7443148169db00020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000008ac7230489e80000000000000000000000000000000000000000000000000000000000000000011a000000000000000000000000000000000000000000000000000000000000015606000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e000200000000000000000de0b6b3a7640000000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e0004000000000000000002c68af0bb1400000000000000000000000000008a5bbc20ad253e296f61601e868a3206b2d4774c0002000000000000000002c68af0bb1400000000000000000000000000003d9e7a12daa29a8b2b1bfaa9dc97ce018853ab31000400000000000000000de0b6b3a7640000000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e0004000000000000000002c68af0bb140000000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e0004000000000000000002c68af0bb14000000000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b00000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b";
//saleSealed vaa 0x01000000000100db9b30d6ea7eca3a68090ac1d494c188c75c4878e6c79e722d7b5b8b8b5ec4dd0f375eafa7aee0950f3f5976c6afb30ea7fe1f1ae5094571259d1b2cc2cd9ef900000001850000000000020000000000000000000000005f8e26facc23fa4cbd87b8d9dbbd33d5047abde100000000000000010f0300000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000047a09633e414a520100000000000000000000000000000000000000000000000002fc06415c6980000200000000000000000000000000000000000000000000000000729a89eca021080300000000000000000000000000000000000000000000000003bb07d1b383e0000400000000000000000000000000000000000000000000000000e53511bee2f000050000000000000000000000000000000000000000000000000157cf9cf2604c00
//second sale: aborted
//initSale vaa 0x01000000000100fdcba5d7965018ee9d02eb8cdedcd8dafa96f9be0ba5c3703701f725d38595bd290943517bca627f9805fe7dfef63e7f32ea073363c6ddd7832cee4aebda40f501000001c60000000000020000000000000000000000005f8e26facc23fa4cbd87b8d9dbbd33d5047abde100000000000000020f0100000000000000000000000000000000000000000000000000000000000000010000000000000000000000005f9d8f5c2648220bc45ba9eea6adb8c38920494300020000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000008ac7230489e8000000000000000000000000000000000000000000000000000000000000000001c9000000000000000000000000000000000000000000000000000000000000020506000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e000200000000000000000de0b6b3a7640000000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e0004000000000000000002c68af0bb1400000000000000000000000000008a5bbc20ad253e296f61601e868a3206b2d4774c0002000000000000000002c68af0bb1400000000000000000000000000003d9e7a12daa29a8b2b1bfaa9dc97ce018853ab31000400000000000000000de0b6b3a7640000000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e0004000000000000000002c68af0bb140000000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e0004000000000000000002c68af0bb14000000000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b00000000000000000000000022d491bde2303f2f43325b2108d26f1eaba1e32b
//saleAborted vaa 0x010000000001002a357b33c992d88135f19dad31f0f32b10ed670a50550f21c37026501a990c313d4fdb9cb60dee8e9b8a84dd94baa1a6bca8c472b12c1906f4fb321eb08b5819010000021e0000000000020000000000000000000000005f8e26facc23fa4cbd87b8d9dbbd33d5047abde100000000000000030f040000000000000000000000000000000000000000000000000000000000000001
import { describe, expect, jest, test, xtest } from "@jest/globals";
import {
CHAIN_ID_BSC,
CHAIN_ID_ETH,
ixFromRust,
setDefaultWasm,
sleepFor,
postVaaSolanaWithRetry,
} from "../..";
import {
vaa_address,
init_icco_sale_ix,
} from "../../solana/icco_contributor-node";
// Solana
import { Token, TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { Keypair, Connection, PublicKey, Transaction } from "@solana/web3.js";
import { ethers } from "ethers";
import { getContributorContractAsHexStringOnEth } from "../getters";
import {
BSC_NODE_URL,
ETH_NODE_URL,
ETH_PRIVATE_KEY1,
ETH_PRIVATE_KEY2,
ETH_PRIVATE_KEY3,
ETH_PRIVATE_KEY4,
ETH_PRIVATE_KEY5,
ETH_TOKEN_BRIDGE_ADDRESS,
ETH_TOKEN_SALE_CONDUCTOR_ADDRESS,
ETH_TOKEN_SALE_CONTRIBUTOR_ADDRESS,
TEST_ERC20,
WBNB_ADDRESS,
WETH_ADDRESS,
} from "../__tests__/consts";
import {
createSaleOnEthAndGetVaa,
EthBuyerConfig,
EthContributorConfig,
// createSaleOnEthAndInit,
// waitForSaleToEnd,
// waitForSaleToStart,
makeAcceptedTokensFromConfigs,
// sealOrAbortSaleOnEth,
// contributeAllTokensOnEth,
// secureContributeAllTokensOnEth,
// getCollateralBalancesOnEth,
// claimAllAllocationsOnEth,
// getAllocationBalancesOnEth,
// contributionsReconcile,
// allocationsReconcile,
// claimAllBuyerRefundsOnEth,
// refundsReconcile,
// prepareBuyersForMixedContributionTest,
makeSaleStartFromLastBlock,
// sealSaleAtContributors,
// abortSaleAtContributors,
// claimConductorRefund,
// claimOneContributorRefundOnEth,
// redeemCrossChainAllocations,
// attestSaleToken,
// getWrappedCollateral,
// getRefundRecipientBalanceOnEth,
// abortSaleEarlyAtContributors,
// abortSaleEarlyAtConductor,
deployTokenOnEth,
} from "../__tests__/helpers";
setDefaultWasm("node");
//import { init_icco_sale_ix } from "icco_contributor";
const SOLANA_WALLET_PK =
"14,173,153,4,176,224,201,111,32,237,183,185,159,247,22,161,89,84,215,209,212,137,10,92,157,49,29,192,101,164,152,70,87,65,8,174,214,157,175,126,98,90,54,24,100,177,247,77,19,112,47,44,165,109,233,102,14,86,109,29,134,145,132,141";
const SOLANA_CONTRIBUTOR_ADDR = "5yrpFgtmiBkRmDgveVErMWuxC25eK5QE5ouZgfi46aqM";
const SOLANA_BRIDGE_ADDR = "Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o";
// ten minutes? nobody got time for that
jest.setTimeout(60000);
const solanaConnection = new Connection(
"http://localhost:8899",
"singleGossip"
);
describe("Solana dev Tests", () => {
// This is just mock call into solana icco contract to pass hardcoded VAA. It won't pass ovnership test.
test("call into init_icco_sale with hardcoded VAA", (done) => {
(async () => {
// try {
console.log("bbrp -->> hc init_icco_sale");
// Wallet (payer) account decode
const privateKeyDecoded = Uint8Array.from(
SOLANA_WALLET_PK.split(",").map((s) => parseInt(s))
);
// console.log(privateKeyDecoded);
const walletAccount = Keypair.fromSecretKey(privateKeyDecoded);
console.log(walletAccount.publicKey.toString()); // check "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J"
const saleInitVaa = Uint8Array.from(Buffer.from(initSaleVaa, "hex")); // Hardcoded!
console.log("bbrp vaa len: ", saleInitVaa.length);
// Log Solana VAA PDA address.
const vaa_pda = vaa_address(SOLANA_BRIDGE_ADDR, saleInitVaa);
const vaa_pda_pk = new PublicKey(vaa_pda);
console.log("bbrp vaa PDA: ", vaa_pda_pk.toString());
// Make init_icco_sale_ix.
const ix = ixFromRust(
init_icco_sale_ix(
SOLANA_CONTRIBUTOR_ADDR,
SOLANA_BRIDGE_ADDR,
walletAccount.publicKey.toString(),
saleInitVaa
)
);
// call contributor contract
const tx = new Transaction().add(ix);
await solanaConnection.sendTransaction(tx, [walletAccount], {
skipPreflight: false,
preflightCommitment: "singleGossip",
});
// Done here.
console.log("bbrp <<--- hc init_icco_sale");
done();
// } catch (e) {
// console.error(e);
// done("An error occurred in init_icco_sale contributor test");
// }
})();
});
xtest("call into init_icco_sale", (done) => {
(async () => {
try {
console.log("bbrp -->> init_icco_sale");
// create initSale using conductor on ETH.
const ethProvider = new ethers.providers.WebSocketProvider(
ETH_NODE_URL
);
const contributorConfigs: EthContributorConfig[] = [
{
chainId: CHAIN_ID_ETH,
wallet: new ethers.Wallet(ETH_PRIVATE_KEY1, ethProvider),
collateralAddress: WETH_ADDRESS,
conversionRate: "1",
},
// TBD Need to add solana?
];
const conductorConfig = contributorConfigs[0];
// make sale token. mint 10 and sell 10%
const tokenAddress = await deployTokenOnEth(
ETH_NODE_URL,
"Icco-Test",
"ICCO",
ethers.utils.parseUnits("10").toString(),
conductorConfig.wallet
);
console.log("Token Address: ", tokenAddress);
const buyers: EthBuyerConfig[] = [
// native weth
{
chainId: CHAIN_ID_ETH,
wallet: new ethers.Wallet(ETH_PRIVATE_KEY2, ethProvider),
collateralAddress: WETH_ADDRESS,
contribution: "6",
tokenIndex: 0,
},
];
// we need to set up all of the accepted tokens (natives plus their wrapped versions)
const acceptedTokens = await makeAcceptedTokensFromConfigs(
contributorConfigs,
buyers
);
const tokenAmount = "1";
const minRaise = "10"; // eth units
const maxRaise = "14";
const saleDuration = 60; // seconds
// get the time
const saleStart = await makeSaleStartFromLastBlock(contributorConfigs);
const decimals = 9;
const saleEnd = saleStart + saleDuration;
const saleInitVaa = await createSaleOnEthAndGetVaa(
conductorConfig.wallet,
conductorConfig.chainId,
tokenAddress,
ethers.utils.parseUnits(tokenAmount, decimals),
ethers.utils.parseUnits(minRaise),
ethers.utils.parseUnits(maxRaise),
saleStart,
saleEnd,
acceptedTokens
);
console.info(
"Sale Init VAA:",
Buffer.from(saleInitVaa).toString("hex")
);
// Wallet (payer) account decode
const privateKeyDecoded = Uint8Array.from(
SOLANA_WALLET_PK.split(",").map((s) => parseInt(s))
);
// console.log(privateKeyDecoded);
const walletAccount = Keypair.fromSecretKey(privateKeyDecoded);
// console.log(walletAccount.publicKey.toString()); // check "6sbzC1eH4FTujJXWj51eQe25cYvr4xfXbJ1vAj7j2k5J"
// Log Solana VAA PDA address.
const vaa_pda = vaa_address(SOLANA_BRIDGE_ADDR, saleInitVaa);
const vaa_pda_pk = new PublicKey(vaa_pda);
console.log("bbrp vaa PDA: ", vaa_pda_pk.toString());
// post VAA on solana.
await postVaaSolanaWithRetry(
solanaConnection,
async (transaction) => {
transaction.partialSign(walletAccount);
return transaction;
},
SOLANA_BRIDGE_ADDR,
walletAccount.publicKey.toString(),
Buffer.from(saleInitVaa),
0
);
// const saleInitVaa = Uint8Array.from(Buffer.from(initSaleVaa, "hex"));
// console.log("bbrp vaa len: ", saleInitVaa.length);
// Make init_icco_sale_ix.
const ix = ixFromRust(
init_icco_sale_ix(
SOLANA_CONTRIBUTOR_ADDR,
SOLANA_BRIDGE_ADDR,
walletAccount.publicKey.toString(),
saleInitVaa
)
);
// call contributor contract
const tx = new Transaction().add(ix);
await solanaConnection.sendTransaction(tx, [walletAccount], {
skipPreflight: false,
preflightCommitment: "singleGossip",
});
// Done here.
ethProvider.destroy();
console.log("bbrp <<--- init_icco_sale");
done();
} catch (e) {
console.error(e);
done("An error occurred in init_icco_sale contributor test");
}
})();
});
});

View File

@ -1,141 +0,0 @@
import { ethers } from "ethers";
import {
ChainId,
ERC20__factory,
IWETH__factory,
hexToUint8Array,
nativeToHexString,
getForeignAssetEth,
getOriginalAssetEth,
uint8ArrayToNative,
} from "@certusone/wormhole-sdk";
import { parseUnits } from "ethers/lib/utils";
export function nativeToUint8Array(
address: string,
chainId: ChainId
): Uint8Array {
return hexToUint8Array(nativeToHexString(address, chainId) || "");
}
export async function wrapEth(
wethAddress: string,
amount: string,
wallet: ethers.Wallet
): Promise<void> {
const weth = IWETH__factory.connect(wethAddress, wallet);
await weth.deposit({
value: ethers.utils.parseUnits(amount),
});
}
export async function getCurrentBlock(
provider: ethers.providers.Provider
): Promise<ethers.providers.Block> {
const currentBlockNumber = await provider.getBlockNumber();
return provider.getBlock(currentBlockNumber);
}
export async function sleepFor(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function getErc20Balance(
provider: ethers.providers.Provider,
tokenAddress: string,
walletAddress: string
): Promise<ethers.BigNumber> {
const token = ERC20__factory.connect(tokenAddress, provider);
return token.balanceOf(walletAddress);
}
export async function getErc20Decimals(
provider: ethers.providers.Provider,
tokenAddress: string
): Promise<number> {
const token = ERC20__factory.connect(tokenAddress, provider);
return token.decimals();
}
export async function getAcceptedTokenDecimalsOnConductor(
contributorChain: ChainId,
conductorChain: ChainId,
contributorTokenBridgeAddress: string,
conductorTokenBridgeAddress: string,
contributorProvider: ethers.providers.Provider,
conductorProvider: ethers.providers.Provider,
contributedTokenAddress: string,
condtributedTokenDecimals: number
): Promise<number> {
if (contributorChain !== conductorChain) {
// fetch the original token address for contributed token
const originalToken = await getOriginalAssetEth(
contributorTokenBridgeAddress,
contributorProvider,
contributedTokenAddress,
contributorChain
);
let tokenDecimalsOnConductor;
if (originalToken.chainId === conductorChain) {
// get the original decimals
const nativeConductorAddress = uint8ArrayToNative(
originalToken.assetAddress,
originalToken.chainId
);
if (nativeConductorAddress !== undefined) {
// fetch the token decimals on the conductor chain
tokenDecimalsOnConductor = await getErc20Decimals(
conductorProvider,
nativeConductorAddress
);
} else {
throw Error("Native conductor address is undefined");
}
} else {
// get the wrapped versionals decimals on eth
const conductorWrappedToken = await getForeignAssetEth(
conductorTokenBridgeAddress,
conductorProvider,
originalToken.chainId,
originalToken.assetAddress
);
if (conductorWrappedToken !== null) {
// fetch the token decimals on the conductor chain
tokenDecimalsOnConductor = await getErc20Decimals(
conductorProvider,
conductorWrappedToken
);
} else {
throw Error("Wrapped conductor address is null");
}
}
return tokenDecimalsOnConductor;
} else {
return condtributedTokenDecimals;
}
}
export async function normalizeConversionRate(
denominationDecimals: number,
acceptedTokenDecimals: number,
conversionRate: string,
conductorDecimals: number
): Promise<ethers.BigNumberish> {
const precision = 18;
const normDecimals = denominationDecimals + precision - acceptedTokenDecimals;
let normalizedConversionRate = parseUnits(conversionRate, normDecimals);
if (acceptedTokenDecimals === conductorDecimals) {
return normalizedConversionRate;
} else if (acceptedTokenDecimals > conductorDecimals) {
return normalizedConversionRate.div(
parseUnits("1", acceptedTokenDecimals - conductorDecimals)
);
} else {
return normalizedConversionRate.mul(
parseUnits("1", conductorDecimals - acceptedTokenDecimals)
);
}
}

View File

@ -1,179 +0,0 @@
import { ethers } from "ethers";
import { ChainId, uint8ArrayToHex } from "@certusone/wormhole-sdk";
import {
AcceptedToken,
Allocation,
SaleInit,
SolanaSaleInit,
SolanaToken,
SaleSealed,
} from "./structs";
const VAA_PAYLOAD_NUM_ACCEPTED_TOKENS = 228;
const VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH = 50;
export async function getSaleIdFromIccoVaa(
payload: Uint8Array
): Promise<ethers.BigNumberish> {
return ethers.BigNumber.from(payload.slice(1, 33)).toString();
}
export async function getTargetChainIdFromTransferVaa(
payload: Uint8Array
): Promise<ChainId> {
return Buffer.from(payload).readUInt16BE(99) as ChainId;
}
export async function parseSaleInit(payload: Uint8Array): Promise<SaleInit> {
const buffer = Buffer.from(payload);
const numAcceptedTokens = buffer.readUInt8(VAA_PAYLOAD_NUM_ACCEPTED_TOKENS);
const recipientIndex =
VAA_PAYLOAD_NUM_ACCEPTED_TOKENS +
numAcceptedTokens * VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH +
1;
return {
payloadId: buffer.readUInt8(0),
saleId: ethers.BigNumber.from(payload.slice(1, 33)).toString(),
tokenAddress: uint8ArrayToHex(payload.slice(33, 65)),
tokenChain: buffer.readUInt16BE(65),
tokenDecimals: buffer.readUInt8(67),
tokenAmount: ethers.BigNumber.from(payload.slice(68, 100)).toString(),
minRaise: ethers.BigNumber.from(payload.slice(100, 132)).toString(),
maxRaise: ethers.BigNumber.from(payload.slice(132, 164)).toString(),
saleStart: ethers.BigNumber.from(payload.slice(164, 196)).toString(),
saleEnd: ethers.BigNumber.from(payload.slice(196, 228)).toString(),
acceptedTokens: parseAcceptedTokens(payload, numAcceptedTokens),
solanaTokenAccount: uint8ArrayToHex(
payload.slice(recipientIndex, recipientIndex + 32)
),
recipient: uint8ArrayToHex(
payload.slice(recipientIndex + 32, recipientIndex + 64)
),
refundRecipient: uint8ArrayToHex(
payload.slice(recipientIndex + 64, recipientIndex + 96)
),
};
}
function parseAcceptedTokens(
payload: Uint8Array,
numTokens: number
): AcceptedToken[] {
const buffer = Buffer.from(payload);
const tokens: AcceptedToken[] = [];
for (let i = 0; i < numTokens; ++i) {
const startIndex =
VAA_PAYLOAD_NUM_ACCEPTED_TOKENS +
1 +
i * VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH;
const token: AcceptedToken = {
tokenAddress: uint8ArrayToHex(payload.slice(startIndex, startIndex + 32)),
tokenChain: buffer.readUInt16BE(startIndex + 32),
conversionRate: ethers.BigNumber.from(
payload.slice(
startIndex + 34,
startIndex + VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH
)
).toString(),
};
tokens.push(token);
}
return tokens;
}
const SOLANA_VAA_PAYLOAD_NUM_ACCEPTED_TOKENS = 132;
const SOLANA_VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH = 33;
export async function parseSolanaSaleInit(
payload: Uint8Array
): Promise<SolanaSaleInit> {
const buffer = Buffer.from(payload);
const numAcceptedTokens = buffer.readUInt8(
SOLANA_VAA_PAYLOAD_NUM_ACCEPTED_TOKENS
);
const recipientIndex =
SOLANA_VAA_PAYLOAD_NUM_ACCEPTED_TOKENS +
numAcceptedTokens * SOLANA_VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH +
1;
return {
payloadId: buffer.readUInt8(0),
saleId: ethers.BigNumber.from(payload.slice(1, 33)).toString(),
solanaTokenAccount: uint8ArrayToHex(payload.slice(33, 65)),
tokenChain: buffer.readUInt16BE(65),
tokenDecimals: buffer.readUInt8(67),
saleStart: ethers.BigNumber.from(payload.slice(68, 100)).toString(),
saleEnd: ethers.BigNumber.from(payload.slice(100, 132)).toString(),
acceptedTokens: parseSolanaAcceptedTokens(payload, numAcceptedTokens),
recipient: uint8ArrayToHex(
payload.slice(recipientIndex, recipientIndex + 32)
),
};
}
function parseSolanaAcceptedTokens(
payload: Uint8Array,
numTokens: number
): SolanaToken[] {
const buffer = Buffer.from(payload);
const tokens: SolanaToken[] = [];
for (let i = 0; i < numTokens; ++i) {
const startIndex =
SOLANA_VAA_PAYLOAD_NUM_ACCEPTED_TOKENS +
1 +
i * SOLANA_VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH;
const token: SolanaToken = {
tokenIndex: buffer.readUInt8(startIndex),
tokenAddress: uint8ArrayToHex(
payload.slice(startIndex + 1, startIndex + 33)
),
};
tokens.push(token);
}
return tokens;
}
const VAA_PAYLOAD_NUM_ALLOCATIONS = 33;
const VAA_PAYLOAD_ALLOCATION_BYTES_LENGTH = 65;
export async function parseSaleSealed(
payload: Uint8Array
): Promise<SaleSealed> {
const buffer = Buffer.from(payload);
const numAllocations = buffer.readUInt8(VAA_PAYLOAD_NUM_ALLOCATIONS);
return {
payloadId: buffer.readUInt8(0),
saleId: ethers.BigNumber.from(payload.slice(1, 33)).toString(),
allocations: parseAllocations(payload, numAllocations),
};
}
function parseAllocations(
payload: Uint8Array,
numAllocations: number
): Allocation[] {
const buffer = Buffer.from(payload);
const allocations: Allocation[] = [];
for (let i = 0; i < numAllocations; ++i) {
const startIndex =
VAA_PAYLOAD_NUM_ALLOCATIONS + 1 + i * VAA_PAYLOAD_ALLOCATION_BYTES_LENGTH;
const allocation: Allocation = {
tokenIndex: buffer.readUInt8(startIndex),
allocation: ethers.BigNumber.from(
payload.slice(startIndex + 1, startIndex + 33)
).toString(),
excessContribution: ethers.BigNumber.from(
payload.slice(startIndex + 33, startIndex + 65)
).toString(),
};
allocations.push(allocation);
}
return allocations;
}

View File

@ -1,7 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./lib/cjs"
}
}

View File

@ -1,22 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"module": "esnext",
"moduleResolution": "node",
"declaration": true,
"outDir": "./lib/esm",
"strict": true,
"esModuleInterop": true,
"downlevelIteration": true,
"allowJs": true,
"resolveJsonModule": true,
"lib": ["dom", "es5", "scripthost", "es2020.bigint"]
},
"include": ["src", "types"],
"exclude": [
"node_modules",
"**/__tests__/*",
"**/__icco_tests__/*",
"**/__tests__wip/*"
]
}

8331
sdk/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

68
sdk/package.json Normal file
View File

@ -0,0 +1,68 @@
{
"name": "@certusone/wormhole-icco-sdk",
"version": "0.1.0",
"description": "SDK for interacting with ICCO",
"homepage": "https://wormholenetwork.com",
"main": "./lib/cjs/index.js",
"module": "./lib/esm/index.js",
"files": [
"lib/"
],
"repository": "https://github.com/certusone/wormhole-icco/tree/main/sdk",
"scripts": {
"clean": "rm -rf node_modules evm-contracts target src/ethers-contracts src/anchor",
"build": "bash scripts/copy_ethereum.sh && bash scripts/copy_anchor.sh",
"testnet-evm-test": "ts-mocha src/testnet/run_evm_sales.ts -t 1000000",
"testnet-solana-test": "ts-mocha src/testnet/run_solana_sales.ts -t 10000000 --exit",
"format": "echo \"disabled: prettier --write \"src/**/*.ts\"\"",
"lint": "tslint -p tsconfig.json",
"prepublishOnly": "echo \"disabled: npm test && npm run lint\"",
"preversion": "npm run lint",
"version": "npm run format && git add -A src",
"postversion": "git push && git push --tags"
},
"keywords": [
"wormhole",
"portal",
"icco",
"ico",
"ido",
"sdk",
"solana",
"ethereum",
"terra",
"bsc",
"polygon",
"avax",
"fantom",
"aurora"
],
"author": "certusone",
"license": "Apache-2.0",
"dependencies": {
"@certusone/wormhole-sdk": "^0.3.4",
"@improbable-eng/grpc-web-node-http-transport": "^0.15.0",
"@openzeppelin/contracts": "^4.2.0",
"@project-serum/anchor": "^0.24.2",
"@solana/spl-token": "^0.2.0",
"@typechain/ethers-v5": "^8.0.0",
"@types/yargs": "^17.0.10",
"byteify": "^2.0.10",
"elliptic": "^6.5.4",
"ethers": "^5.6.8",
"keccak256": "^1.0.6",
"ts-node": "^10.8.1",
"web3-utils": "^1.7.3",
"yargs": "^17.5.1"
},
"devDependencies": {
"@types/bn.js": "^5.1.0",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.0.0",
"chai": "^4.3.4",
"mocha": "^9.0.3",
"prettier": "^2.6.2",
"ts-mocha": "^10.0.0",
"typescript": "^4.3.5"
}
}

1349
sdk/rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +0,0 @@
[workspace]
members = [
"core",
"sdk"
]

View File

@ -1,26 +0,0 @@
[package]
name = "wormhole-core"
version = "0.1.0"
edition = "2018"
[features]
[profile.release]
opt-level = 3
lto = "thin"
[dependencies]
byteorder = "*"
hex = "*"
nom = { version="7", default-features=false, features=["alloc"] }
primitive-types = { version="0.9.0", default-features=false }
sha3 = "0.9.1"
bstr = "*"
[dev-dependencies]
byteorder = "*"
hex = "*"

View File

@ -1,42 +0,0 @@
//! Exposes an API implementation depending on which feature flags have been toggled for the
//! library. Check submodules for chain runtime specific documentation.
use std::convert::TryFrom; // Remove in 2021
/// Chain contains a mapping of Wormhole supported chains to their u16 representation. These are
/// universally defined among all Wormhole contracts.
#[repr(u16)]
#[derive(Clone, Debug, PartialEq)]
pub enum Chain {
All = 0,
Solana = 1,
Ethereum = 2,
Terra = 3,
Binance = 4,
Polygon = 5,
AVAX = 6,
Oasis = 7,
}
impl TryFrom<u16> for Chain {
type Error = ();
fn try_from(other: u16) -> Result<Chain, Self::Error> {
match other {
0 => Ok(Chain::All),
1 => Ok(Chain::Solana),
2 => Ok(Chain::Ethereum),
3 => Ok(Chain::Terra),
4 => Ok(Chain::Binance),
5 => Ok(Chain::Polygon),
6 => Ok(Chain::AVAX),
7 => Ok(Chain::Oasis),
_ => Err(()),
}
}
}
impl Default for Chain {
fn default() -> Self {
Self::All
}
}

View File

@ -1,22 +0,0 @@
/// Ergonomic error handler for use within the Wormhole core/SDK libraries.
#[macro_export]
macro_rules! require {
($expr:expr, $name:ident) => {
if !$expr {
return Err($name.into());
}
}
}
/// This ErrorCode maps to the nom ParseError, we use an integer because the library is deprecating
/// the current error type, so we should avoid depending on it for now.
type ErrorCode = usize;
#[derive(Debug)]
pub enum WormholeError {
InvalidGovernanceAction,
InvalidGovernanceChain,
InvalidGovernanceModule,
DeserializeFailed,
ParseError(ErrorCode),
}

View File

@ -1,36 +0,0 @@
#![deny(unused_results)]
pub use chain::*;
pub use error::*;
pub use vaa::*;
pub mod chain;
pub mod vaa;
#[macro_use]
pub mod error;
/// Helper method that attempts to parse and truncate UTF-8 from a byte stream. This is useful when
/// the wire data is expected to contain UTF-8 that is either already truncated, or needs to be,
/// while still maintaining the ability to render.
///
/// This should be used to parse any Text-over-Wormhole fields that are meant to be human readable.
pub(crate) fn parse_fixed_utf8<T: AsRef<[u8]>, const N: usize>(s: T) -> Option<String> {
use bstr::ByteSlice;
use std::io::Cursor;
use std::io::Read;
// Read Bytes.
let mut cursor = Cursor::new(s.as_ref());
let mut buffer = vec![0u8; N];
cursor.read_exact(&mut buffer).ok()?;
buffer.retain(|&c| c != 0);
// Attempt UTF-8 Decoding. Stripping invalid Unicode characters (0xFFFD).
let mut buffer: Vec<char> = buffer.chars().collect();
buffer.retain(|&c| c != '\u{FFFD}');
Some(buffer.iter().collect())
}

View File

@ -1,385 +0,0 @@
//! VAA's represent a collection of signatures combined with a message and its metadata. VAA's are
//! used as a form of proof; by submitting a VAA to a target contract, the receiving contract can
//! make assumptions about the validity of state on the source chain.
//!
//! Wormhole defines several VAA's for use within Token/NFT bridge implemenetations, as well as
//! governance specific VAA's used within Wormhole's guardian network.
//!
//! This module provides definitions and parsers for all current Wormhole standard VAA's, and
//! includes parsers for the core VAA type. Programs targetting wormhole can use this module to
//! parse and verify incoming VAA's securely.
use nom::combinator::rest;
use nom::error::{
Error,
ErrorKind,
};
use nom::multi::{
count,
fill,
};
use nom::number::complete::{
u16,
u32,
u64,
u8,
};
use nom::number::Endianness;
use nom::{
Err,
Finish,
IResult,
};
use std::convert::TryFrom;
use std::str::FromStr; // Remove in 2021
use crate::WormholeError::{
InvalidGovernanceAction,
InvalidGovernanceChain,
InvalidGovernanceModule,
};
use crate::{
require,
Chain,
WormholeError,
};
// Import Module Specific VAAs.
pub mod core;
pub mod nft;
pub mod token;
/// Signatures are typical ECDSA signatures prefixed with a Guardian position. These have the
/// following byte layout:
/// ```markdown
/// 0 .. 1: Guardian No.
/// 1 .. 65: Signature (ECDSA)
/// 65 .. 66: Recovery ID (ECDSA)
/// ```
pub type Signature = [u8; 66];
/// Wormhole specifies token addresses as 32 bytes. Addresses that are shorter, for example 20 byte
/// Ethereum addresses, are left zero padded to 32.
pub type ForeignAddress = [u8; 32];
/// Fields on VAA's are all usually fixed bytestrings, however they often contain UTF-8. When
/// parsed these result in `String` with the additional constraint that they are always equal or
/// less to the underlying byte field.
type ShortUTFString = String;
/// The core VAA itself. This structure is what is received by a contract on the receiving side of
/// a wormhole message passing flow. The payload of the message must be parsed separately to the
/// VAA itself as it is completely user defined.
#[derive(Debug, Default, PartialEq)]
pub struct VAA {
// Header
pub version: u8,
pub guardian_set_index: u32,
pub signatures: Vec<Signature>,
// Body
pub timestamp: u32,
pub nonce: u32,
pub emitter_chain: Chain,
pub emitter_address: ForeignAddress,
pub sequence: u64,
pub consistency_level: u8,
pub payload: Vec<u8>,
}
/// Contains the hash, secp256k1 payload, and serialized digest of the VAA. These are used in
/// various places in Wormhole codebases.
pub struct VAADigest {
pub digest: Vec<u8>,
pub hash: [u8; 32],
}
impl VAA {
/// Given any argument treatable as a series of bytes, attempt to deserialize into a valid VAA.
pub fn from_bytes<T: AsRef<[u8]>>(input: T) -> Result<Self, WormholeError> {
match parse_vaa(input.as_ref()).finish() {
Ok(input) => Ok(input.1),
Err(e) => Err(WormholeError::ParseError(e.code as usize)),
}
}
/// A VAA is distinguished by the unique hash of its deterministic components. This method
/// returns a 256 bit Keccak hash of these components. This hash is utilised in all Wormhole
/// components for identifying unique VAA's, including the bridge, modules, and core guardian
/// software.
pub fn digest(&self) -> Option<VAADigest> {
use byteorder::{
BigEndian,
WriteBytesExt,
};
use sha3::Digest;
use std::io::{
Cursor,
Write,
};
// Hash Deterministic Pieces
let body = {
let mut v = Cursor::new(Vec::new());
v.write_u32::<BigEndian>(self.timestamp).ok()?;
v.write_u32::<BigEndian>(self.nonce).ok()?;
v.write_u16::<BigEndian>(self.emitter_chain.clone() as u16).ok()?;
let _ = v.write(&self.emitter_address).ok()?;
v.write_u64::<BigEndian>(self.sequence).ok()?;
v.write_u8(self.consistency_level).ok()?;
let _ = v.write(&self.payload).ok()?;
v.into_inner()
};
// We hash the body so that secp256k1 signatures are signing the hash instead of the body
// within our contracts. We do this so we don't have to submit the entire VAA for signature
// verification, only the hash.
let hash: [u8; 32] = {
let mut h = sha3::Keccak256::default();
let _ = h.write(body.as_slice()).unwrap();
h.finalize().into()
};
Some(VAADigest {
digest: body,
hash,
})
}
}
/// Using nom, parse a fixed array of bytes without any allocation. Useful for parsing addresses,
/// signatures, identifiers, etc.
#[inline]
pub fn parse_fixed<const S: usize>(input: &[u8]) -> IResult<&[u8], [u8; S]> {
let mut buffer = [0u8; S];
let (i, _) = fill(u8, &mut buffer)(input)?;
Ok((i, buffer))
}
/// Parse a Chain ID, which is a 16 bit numeric ID. The mapping of network to ID is defined by the
/// Wormhole standard.
#[inline]
pub fn parse_chain(input: &[u8]) -> IResult<&[u8], Chain> {
let (i, chain) = u16(Endianness::Big)(input)?;
let chain = Chain::try_from(chain).map_err(|_| Err::Error(Error::new(i, ErrorKind::NoneOf)))?;
Ok((i, chain))
}
/// Parse a VAA from a vector of raw bytes. Nom handles situations where the data is either too
/// short or too long.
#[inline]
fn parse_vaa(input: &[u8]) -> IResult<&[u8], VAA> {
let (i, version) = u8(input)?;
let (i, guardian_set_index) = u32(Endianness::Big)(i)?;
let (i, signature_count) = u8(i)?;
let (i, signatures) = count(parse_fixed, signature_count.into())(i)?;
let (i, timestamp) = u32(Endianness::Big)(i)?;
let (i, nonce) = u32(Endianness::Big)(i)?;
let (i, emitter_chain) = parse_chain(i)?;
let (i, emitter_address) = parse_fixed(i)?;
let (i, sequence) = u64(Endianness::Big)(i)?;
let (i, consistency_level) = u8(i)?;
let (i, payload) = rest(i)?;
Ok((
i,
VAA {
version,
guardian_set_index,
signatures,
timestamp,
nonce,
emitter_chain,
emitter_address,
sequence,
consistency_level,
payload: payload.to_vec(),
},
))
}
/// All current Wormhole programs using Governance are prefixed with a Governance header with a
/// consistent format.
pub struct GovHeader {
pub module: [u8; 32],
pub action: u8,
pub chains: Chain,
}
pub trait GovernanceAction: Sized {
const ACTION: u8;
const MODULE: &'static [u8];
/// Implement a nom parser for the Action.
fn parse(input: &[u8]) -> IResult<&[u8], Self>;
/// Serialize to Wormhole wire format.
/// fn serialize(&self) -> Result<Vec<u8>, WormholeError>;
/// Parses an Action from a governance payload securely.
fn from_bytes<T: AsRef<[u8]>>(
input: T,
chain: Option<Chain>,
) -> Result<(GovHeader, Self), WormholeError> {
match parse_action(input.as_ref()).finish() {
Ok((_, (header, action))) => {
// If no Chain is given, we assume All, which implies always valid.
let chain = chain.unwrap_or(Chain::All);
// Left 0-pad the MODULE in case it is unpadded.
let mut module = [0u8; 32];
let modlen = Self::MODULE.len();
(&mut module[32 - modlen..]).copy_from_slice(&Self::MODULE);
// Verify Governance Data.
let valid_chain = chain == header.chains || chain == Chain::All;
let valid_action = header.action == Self::ACTION;
let valid_module = module == header.module;
require!(valid_action, InvalidGovernanceAction);
require!(valid_chain, InvalidGovernanceChain);
require!(valid_module, InvalidGovernanceModule);
Ok((header, action))
}
Err(e) => Err(WormholeError::ParseError(e.code as usize)),
}
}
}
#[inline]
pub fn parse_action<A: GovernanceAction>(input: &[u8]) -> IResult<&[u8], (GovHeader, A)> {
let (i, header) = parse_governance_header(input.as_ref())?;
let (i, action) = A::parse(i)?;
Ok((i, (header, action)))
}
#[inline]
pub fn parse_governance_header<'i, 'a>(input: &'i [u8]) -> IResult<&'i [u8], GovHeader> {
let (i, module) = parse_fixed(input)?;
let (i, action) = u8(i)?;
let (i, chains) = u16(Endianness::Big)(i)?;
Ok((
i,
GovHeader {
module,
action,
chains: Chain::try_from(chains).unwrap(),
},
))
}
#[cfg(test)]
mod testing {
use super::{
parse_governance_header,
Chain,
VAA,
};
#[test]
fn test_valid_gov_header() {
let signers = hex::decode("00b072505b5b999c1d08905c02e2b6b2832ef72c0ba6c8db4f77fe457ef2b3d053410b1e92a9194d9210df24d987ac83d7b6f0c21ce90f8bc1869de0898bda7e9801").unwrap();
let payload = hex::decode("000000000000000000000000000000000000000000546f6b656e42726964676501000000013b26409f8aaded3f5ddca184695aa6a0fa829b0c85caf84856324896d214ca98").unwrap();
let emitter =
hex::decode("0000000000000000000000000000000000000000000000000000000000000004")
.unwrap();
let module =
hex::decode("000000000000000000000000000000000000000000546f6b656e427269646765")
.unwrap();
// Decode VAA.
let vaa = hex::decode("01000000000100b072505b5b999c1d08905c02e2b6b2832ef72c0ba6c8db4f77fe457ef2b3d053410b1e92a9194d9210df24d987ac83d7b6f0c21ce90f8bc1869de0898bda7e980100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000013c1bfa00000000000000000000000000000000000000000000546f6b656e42726964676501000000013b26409f8aaded3f5ddca184695aa6a0fa829b0c85caf84856324896d214ca98").unwrap();
let vaa = VAA::from_bytes(vaa).unwrap();
// Decode Payload
let (_, header) = parse_governance_header(&vaa.payload).unwrap();
// Confirm Parsed matches Required.
assert_eq!(&header.module, &module[..]);
assert_eq!(header.action, 1);
assert_eq!(header.chains, Chain::All);
}
// Legacy VAA Signature Struct.
#[derive(Default, Clone)]
pub struct VAASignature {
pub signature: Vec<u8>,
pub guardian_index: u8,
}
// Original VAA Parsing Code. Used to compare current code to old for parity.
pub fn legacy_deserialize(data: &[u8]) -> std::result::Result<VAA, std::io::Error> {
use byteorder::{
BigEndian,
ReadBytesExt,
};
use std::convert::TryFrom;
use std::io::Read;
let mut rdr = std::io::Cursor::new(data);
let mut v = VAA {
..Default::default()
};
v.version = rdr.read_u8()?;
v.guardian_set_index = rdr.read_u32::<BigEndian>()?;
let len_sig = rdr.read_u8()?;
let mut sigs: Vec<_> = Vec::with_capacity(len_sig as usize);
for _i in 0..len_sig {
let mut sig = [0u8; 66];
sig[0] = rdr.read_u8()?;
rdr.read_exact(&mut sig[1..66])?;
sigs.push(sig);
}
v.signatures = sigs;
v.timestamp = rdr.read_u32::<BigEndian>()?;
v.nonce = rdr.read_u32::<BigEndian>()?;
v.emitter_chain = Chain::try_from(rdr.read_u16::<BigEndian>()?).unwrap();
let mut emitter_address = [0u8; 32];
rdr.read_exact(&mut emitter_address)?;
v.emitter_address = emitter_address;
v.sequence = rdr.read_u64::<BigEndian>()?;
v.consistency_level = rdr.read_u8()?;
let _ = rdr.read_to_end(&mut v.payload)?;
Ok(v)
}
#[test]
fn test_parse_vaa_parity() {
// Decode VAA with old and new parsers, and compare result.
let vaa = hex::decode("01000000000100b072505b5b999c1d08905c02e2b6b2832ef72c0ba6c8db4f77fe457ef2b3d053410b1e92a9194d9210df24d987ac83d7b6f0c21ce90f8bc1869de0898bda7e980100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000013c1bfa00000000000000000000000000000000000000000000546f6b656e42726964676501000000013b26409f8aaded3f5ddca184695aa6a0fa829b0c85caf84856324896d214ca98").unwrap();
let new = VAA::from_bytes(&vaa).unwrap();
let old = legacy_deserialize(&vaa).unwrap();
assert_eq!(new, old);
}
#[test]
fn test_valid_parse_vaa() {
let signers = hex::decode("00b072505b5b999c1d08905c02e2b6b2832ef72c0ba6c8db4f77fe457ef2b3d053410b1e92a9194d9210df24d987ac83d7b6f0c21ce90f8bc1869de0898bda7e9801").unwrap();
let payload = hex::decode("000000000000000000000000000000000000000000546f6b656e42726964676501000000013b26409f8aaded3f5ddca184695aa6a0fa829b0c85caf84856324896d214ca98").unwrap();
let emitter =
hex::decode("0000000000000000000000000000000000000000000000000000000000000004")
.unwrap();
// Decode VAA.
let vaa = hex::decode("01000000000100b072505b5b999c1d08905c02e2b6b2832ef72c0ba6c8db4f77fe457ef2b3d053410b1e92a9194d9210df24d987ac83d7b6f0c21ce90f8bc1869de0898bda7e980100000001000000010001000000000000000000000000000000000000000000000000000000000000000400000000013c1bfa00000000000000000000000000000000000000000000546f6b656e42726964676501000000013b26409f8aaded3f5ddca184695aa6a0fa829b0c85caf84856324896d214ca98").unwrap();
let vaa = VAA::from_bytes(vaa).unwrap();
// Verify Decoded VAA.
assert_eq!(vaa.version, 1);
assert_eq!(vaa.guardian_set_index, 0);
assert_eq!(vaa.signatures.len(), 1);
assert_eq!(vaa.signatures[0][..], signers);
assert_eq!(vaa.timestamp, 1);
assert_eq!(vaa.nonce, 1);
assert_eq!(vaa.emitter_chain, Chain::Solana);
assert_eq!(vaa.emitter_address, emitter[..]);
assert_eq!(vaa.sequence, 20_716_538);
assert_eq!(vaa.consistency_level, 0);
assert_eq!(vaa.payload, payload);
}
#[test]
fn test_invalid_vaa() {
}
}

View File

@ -1,99 +0,0 @@
//! This module exposes parsers for core bridge VAAs. The main job of the bridge is to forward
//! VAA's to other chains, however governance actions are themselves VAAs and as such the bridge
//! requires parsing Bridge specific VAAs.
//!
//! The core bridge does not define any general VAA's, thus all the payloads in this file are
//! expected to require governance to be executed.
use nom::multi::{
count,
fill,
};
use nom::number::complete::{
u32,
u8,
};
use nom::number::Endianness;
use nom::IResult;
use primitive_types::U256;
use crate::vaa::{
parse_fixed,
GovernanceAction,
};
pub struct GovernanceContractUpgrade {
pub new_contract: [u8; 32],
}
impl GovernanceAction for GovernanceContractUpgrade {
const MODULE: &'static [u8] = b"Core";
const ACTION: u8 = 1;
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (i, new_contract) = parse_fixed(input)?;
Ok((i, Self { new_contract }))
}
}
pub struct GovernanceGuardianSetChange {
pub new_guardian_set_index: u32,
pub new_guardian_set: Vec<[u8; 20]>,
}
impl GovernanceAction for GovernanceGuardianSetChange {
const MODULE: &'static [u8] = b"Core";
const ACTION: u8 = 2;
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (i, new_guardian_set_index) = u32(Endianness::Big)(input)?;
let (i, guardian_count) = u8(i)?;
let (i, new_guardian_set) = count(parse_fixed, guardian_count.into())(i)?;
Ok((
i,
Self {
new_guardian_set_index,
new_guardian_set,
},
))
}
}
pub struct GovernanceSetMessageFee {
pub fee: U256,
}
impl GovernanceAction for GovernanceSetMessageFee {
const MODULE: &'static [u8] = b"Core";
const ACTION: u8 = 3;
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let mut fee = [0u8; 32];
let (i, _) = fill(u8, &mut fee)(input)?;
Ok((
i,
Self {
fee: U256::from_big_endian(&fee),
},
))
}
}
pub struct GovernanceTransferFees {
pub amount: U256,
pub to: [u8; 32],
}
impl GovernanceAction for GovernanceTransferFees {
const MODULE: &'static [u8] = b"Core";
const ACTION: u8 = 4;
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let mut amount = [0u8; 32];
let (i, _) = fill(u8, &mut amount)(input)?;
let (i, to) = parse_fixed(i)?;
Ok((
i,
Self {
amount: U256::from_big_endian(&amount),
to,
},
))
}
}

View File

@ -1,135 +0,0 @@
//! This module exposes parsers for NFT bridge VAAs. Token bridging relies on VAA's that indicate
//! custody/lockup/burn events in order to maintain token parity between multiple chains. These
//! parsers can be used to read these VAAs. It also defines the Governance actions that this module
//! supports, namely contract upgrades and chain registrations.
use nom::bytes::complete::take;
use nom::combinator::verify;
use nom::number::complete::u8;
use nom::{
Finish,
IResult,
};
use primitive_types::U256;
use std::str::from_utf8;
use crate::vaa::{
parse_chain,
parse_fixed,
GovernanceAction,
};
use crate::vaa::ShortUTFString;
use crate::{
Chain,
parse_fixed_utf8,
WormholeError,
};
/// Transfer is a message containing specifics detailing a token lock up on a sending chain. Chains
/// that are attempting to initiate a transfer must lock up tokens in some manner, such as in a
/// custody account or via burning, before emitting this message.
#[derive(PartialEq, Debug, Clone)]
pub struct Transfer {
/// Address of the token. Left-zero-padded if shorter than 32 bytes
pub nft_address: [u8; 32],
/// Chain ID of the token
pub nft_chain: Chain,
/// Symbol of the token
pub symbol: ShortUTFString,
/// Name of the token
pub name: ShortUTFString,
/// TokenID of the token (big-endian uint256)
pub token_id: U256,
/// URI of the token metadata
pub uri: ShortUTFString,
/// Address of the recipient. Left-zero-padded if shorter than 32 bytes
pub to: [u8; 32],
/// Chain ID of the recipient
pub to_chain: Chain,
}
impl Transfer {
pub fn from_bytes<T: AsRef<[u8]>>(input: T) -> Result<Self, WormholeError> {
match parse_payload_transfer(input.as_ref()).finish() {
Ok(input) => Ok(input.1),
Err(e) => Err(WormholeError::ParseError(e.code as usize)),
}
}
}
fn parse_payload_transfer(input: &[u8]) -> IResult<&[u8], Transfer> {
// Parse Payload
let (i, _) = verify(u8, |&s| s == 0x1)(input.as_ref())?;
let (i, nft_address) = parse_fixed(i)?;
let (i, nft_chain) = parse_chain(i)?;
let (i, symbol): (_, [u8; 32]) = parse_fixed(i)?;
let (i, name): (_, [u8; 32]) = parse_fixed(i)?;
let (i, token_id): (_, [u8; 32]) = parse_fixed(i)?;
let (i, uri_len) = u8(i)?;
let (i, uri) = take(uri_len)(i)?;
let (i, to) = parse_fixed(i)?;
let (i, to_chain) = parse_chain(i)?;
// Name/Symbol and URI should be UTF-8 strings, attempt to parse the first two by removing
// invalid bytes -- for the latter, assume UTF-8 and fail if unparseable.
let name = parse_fixed_utf8::<_, 32>(name).unwrap();
let symbol = parse_fixed_utf8::<_, 32>(symbol).unwrap();
let uri = from_utf8(uri).unwrap().to_string();
Ok((
i,
Transfer {
nft_address,
nft_chain,
symbol,
name,
token_id: U256::from_big_endian(&token_id),
uri,
to,
to_chain,
},
))
}
#[derive(PartialEq, Debug)]
pub struct GovernanceRegisterChain {
pub emitter: Chain,
pub endpoint_address: [u8; 32],
}
impl GovernanceAction for GovernanceRegisterChain {
const MODULE: &'static [u8] = b"NFTBridge";
const ACTION: u8 = 1;
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (i, emitter) = parse_chain(input)?;
let (i, endpoint_address) = parse_fixed(i)?;
Ok((
i,
Self {
emitter,
endpoint_address,
},
))
}
}
#[derive(PartialEq, Debug)]
pub struct GovernanceContractUpgrade {
pub new_contract: [u8; 32],
}
impl GovernanceAction for GovernanceContractUpgrade {
const MODULE: &'static [u8] = b"NFTBridge";
const ACTION: u8 = 2;
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (i, new_contract) = parse_fixed(input)?;
Ok((i, Self { new_contract }))
}
}

View File

@ -1,173 +0,0 @@
//! This module exposes parsers for token bridge VAAs. Token bridging relies on VAA's that indicate
//! custody/lockup/burn events in order to maintain token parity between multiple chains. These
//! parsers can be used to read these VAAs. It also defines the Governance actions that this module
//! supports, namely contract upgrades and chain registrations.
use nom::combinator::verify;
use nom::multi::fill;
use nom::number::complete::u8;
use nom::{
Finish,
IResult,
};
use primitive_types::U256;
use crate::vaa::{
GovernanceAction,
parse_chain,
parse_fixed,
ShortUTFString,
};
use crate::{
parse_fixed_utf8,
Chain,
WormholeError,
};
/// Transfer is a message containing specifics detailing a token lock up on a sending chain. Chains
/// that are attempting to initiate a transfer must lock up tokens in some manner, such as in a
/// custody account or via burning, before emitting this message.
#[derive(PartialEq, Debug, Clone)]
pub struct Transfer {
/// Amount being transferred (big-endian uint256)
pub amount: U256,
/// Address of the token. Left-zero-padded if shorter than 32 bytes
pub token_address: [u8; 32],
/// Chain ID of the token
pub token_chain: Chain,
/// Address of the recipient. Left-zero-padded if shorter than 32 bytes
pub to: [u8; 32],
/// Chain ID of the recipient
pub to_chain: Chain,
/// Amount of tokens (big-endian uint256) that the user is willing to pay as relayer fee. Must be <= Amount.
pub fee: U256,
}
impl Transfer {
pub fn from_bytes<T: AsRef<[u8]>>(input: T) -> Result<Self, WormholeError> {
match parse_payload_transfer(input.as_ref()).finish() {
Ok(input) => Ok(input.1),
Err(e) => Err(WormholeError::ParseError(e.code as usize)),
}
}
}
fn parse_payload_transfer(input: &[u8]) -> IResult<&[u8], Transfer> {
// Parser Buffers.
let mut amount = [0u8; 32];
let mut fee = [0u8; 32];
// Parse Payload.
let (i, _) = verify(u8, |&s| s == 0x1)(input)?;
let (i, _) = fill(u8, &mut amount)(i)?;
let (i, token_address) = parse_fixed(i)?;
let (i, token_chain) = parse_chain(i)?;
let (i, to) = parse_fixed(i)?;
let (i, to_chain) = parse_chain(i)?;
let (i, _) = fill(u8, &mut fee)(i)?;
Ok((
i,
Transfer {
amount: U256::from_big_endian(&amount),
token_address,
token_chain,
to,
to_chain,
fee: U256::from_big_endian(&fee),
},
))
}
#[derive(PartialEq, Debug)]
pub struct AssetMeta {
/// Address of the original token on the source chain.
pub token_address: [u8; 32],
/// Source Chain ID.
pub token_chain: Chain,
/// Number of decimals the source token has on its origin chain.
pub decimals: u8,
/// Ticker Symbol for the token on its origin chain.
pub symbol: ShortUTFString,
/// Full Token name for the token on its origin chain.
pub name: ShortUTFString,
}
impl AssetMeta {
pub fn from_bytes<T: AsRef<[u8]>>(input: T) -> Result<Self, WormholeError> {
match parse_payload_asset_meta(input.as_ref()).finish() {
Ok(input) => Ok(input.1),
Err(e) => Err(WormholeError::ParseError(e.code as usize)),
}
}
}
fn parse_payload_asset_meta(input: &[u8]) -> IResult<&[u8], AssetMeta> {
// Parse Payload.
let (i, _) = verify(u8, |&s| s == 0x2)(input.as_ref())?;
let (i, token_address) = parse_fixed(i)?;
let (i, token_chain) = parse_chain(i)?;
let (i, decimals) = u8(i)?;
let (i, symbol): (_, [u8; 32]) = parse_fixed(i)?;
let (i, name): (_, [u8; 32]) = parse_fixed(i)?;
// Name/Symbol should be UTF-8 strings, attempt to parse them by removing invalid bytes.
let symbol = parse_fixed_utf8::<_, 32>(symbol).unwrap();
let name = parse_fixed_utf8::<_, 32>(name).unwrap();
Ok((
i,
AssetMeta {
token_address,
token_chain,
decimals,
symbol,
name,
},
))
}
#[derive(PartialEq, Debug)]
pub struct GovernanceRegisterChain {
pub emitter: Chain,
pub endpoint_address: [u8; 32],
}
impl GovernanceAction for GovernanceRegisterChain {
const MODULE: &'static [u8] = b"TokenBridge";
const ACTION: u8 = 1;
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (i, emitter) = parse_chain(input)?;
let (i, endpoint_address) = parse_fixed(i)?;
Ok((
i,
Self {
emitter,
endpoint_address,
},
))
}
}
#[derive(PartialEq, Debug)]
pub struct GovernanceContractUpgrade {
pub new_contract: [u8; 32],
}
impl GovernanceAction for GovernanceContractUpgrade {
const MODULE: &'static [u8] = b"TokenBridge";
const ACTION: u8 = 2;
fn parse(input: &[u8]) -> IResult<&[u8], Self> {
let (i, new_contract) = parse_fixed(input)?;
Ok((i, Self { new_contract }))
}
}

View File

@ -1,18 +0,0 @@
# Merge similar crates together to avoid multiple use statements.
imports_granularity = "Module"
# Consistency in formatting makes tool based searching/editing better.
empty_item_single_line = false
# Easier editing when arbitrary mixed use statements do not collapse.
imports_layout = "Vertical"
# Default rustfmt formatting of match arms with branches is awful.
match_arm_leading_pipes = "Preserve"
# Align Fields
enum_discrim_align_threshold = 80
struct_field_align_threshold = 80
# Allow up to two blank lines for grouping.
blank_lines_upper_bound = 2

View File

@ -1,62 +0,0 @@
[package]
name = "wormhole-sdk"
version = "0.1.0"
edition = "2018"
[features]
# Helper methods will target the Wormhole mainnet contract addresses.
mainnet = []
# Helper methosd will target the Wormhole devnet contract addresses.
devnet = []
# Enable Optional dependencies that are only required when targetting Terra.
terra = [
"cosmwasm-std",
"cosmwasm-storage",
"schemars",
"serde",
"wormhole-bridge-terra",
]
# Enable Optional dependencies that are only required when targetting Solana.
solana = [
"solana-program",
"wormhole-bridge-solana",
]
[profile.release]
opt-level = 3
lto = "thin"
[dependencies]
borsh = { version="=0.9.1" }
nom = { version="7", default-features=false, features=["alloc"] }
primitive-types = { version = "0.9.0", default-features = false }
wormhole-core = { path="../core", version="0.1.0" }
# Solana Specific
solana-program = { version="=1.9.4", optional=true }
# Terra Specific
cosmwasm-std = { version = "0.16.0", optional=true }
cosmwasm-storage = { version = "0.16.0", optional=true }
schemars = { version = "0.8.1", optional=true }
serde = { version = "1.0.103", default-features = false, features = ["derive"], optional=true }
[dependencies.wormhole-bridge-solana]
path = "../../../solana/bridge/program"
version = "0.1.0"
optional = true
features = [ "no-entrypoint" ]
[dependencies.wormhole-bridge-terra]
path = "../../../terra/contracts/wormhole"
version = "0.1.0"
optional = true
[dev-dependencies]
byteorder = "*"
hex = "*"

View File

@ -1,7 +0,0 @@
[build]
rustflags = [
"-Cpasses=sancov-module",
"-Cllvm-args=-sanitizer-coverage-level=3",
"-Cllvm-args=-sanitizer-coverage-inline-8bit-counters",
"-Zsanitizer=address",
]

File diff suppressed because it is too large Load Diff

View File

@ -1,27 +0,0 @@
[package]
name = "wormhole-sdk-fuzz"
version = "0.0.0"
edition = "2021"
publish = false
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
[dependencies.wormhole-sdk]
path = ".."
features = ["solana", "vaa"]
# Create isolated workspace.
[workspace]
members = ["."]
[[bin]]
name = "vaa"
path = "fuzzers/vaa.rs"
[[bin]]
name = "governance"
path = "fuzzers/governance.rs"

View File

@ -1,8 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use wormhole_sdk::VAA;
fuzz_target!(|data: &[u8]| {
VAA::from_bytes(data);
});

View File

@ -1,8 +0,0 @@
#![no_main]
use libfuzzer_sys::fuzz_target;
use wormhole_sdk::vaa::VAA;
fuzz_target!(|data: &[u8]| {
VAA::from_bytes(data);
});

View File

@ -1,14 +0,0 @@
//! Exposes an API implementation depending on which feature flags have been toggled for the
//! library. Check submodules for chain runtime specific documentation.
#[cfg(feature = "solana")]
pub mod solana;
#[cfg(feature = "solana")]
pub use solana::*;
#[cfg(feature = "terra")]
pub mod terra;
#[cfg(feature = "terra")]
pub use terra::*;

View File

@ -1,134 +0,0 @@
use borsh::BorshDeserialize;
use solana_program::pubkey::Pubkey;
use solana_program::account_info::AccountInfo;
use solana_program::entrypoint::ProgramResult;
use solana_program::program::invoke_signed;
use std::str::FromStr;
// Export Bridge API
pub use bridge::BridgeConfig;
pub use bridge::BridgeData;
pub use bridge::MessageData;
pub use bridge::PostVAAData;
pub use bridge::PostedVAAData;
pub use bridge::VerifySignaturesData;
pub use bridge::instructions;
pub use bridge::solitaire as bridge_entrypoint;
pub use bridge::types::ConsistencyLevel;
use wormhole_core::WormholeError;
use wormhole_core::VAA;
/// Export Core Mainnet Contract Address
#[cfg(feature = "mainnet")]
pub fn id() -> Pubkey {
Pubkey::from_str("worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth").unwrap()
}
/// Export Core Devnet Contract Address
#[cfg(feature = "testnet")]
pub fn id() -> Pubkey {
Pubkey::from_str("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5").unwrap()
}
/// Export Local Tilt Devnet Contract Address
#[cfg(feature = "devnet")]
pub fn id() -> Pubkey {
Pubkey::from_str("Bridge1p5gheXUvJ6jGWGeCsgPKgnE3YgdGKRVCMY9o").unwrap()
}
/// Derives the Wormhole configuration account address.
pub fn config(id: &Pubkey) -> Pubkey {
let (config, _) = Pubkey::find_program_address(&[b"Bridge"], &id);
config
}
/// Derives the Wormhole fee account address, users of the bridge must pay this address before
/// submitting messages to the bridge.
pub fn fee_collector(id: &Pubkey) -> Pubkey {
let (fee_collector, _) = Pubkey::find_program_address(&[b"fee_collector"], &id);
fee_collector
}
/// Derives the sequence address for an emitter, which is incremented after each message post.
pub fn sequence(id: &Pubkey, emitter: &Pubkey) -> Pubkey {
let (sequence, _) = Pubkey::find_program_address(&[b"Sequence", &emitter.to_bytes()], &id);
sequence
}
/// Derives the emitter address for a Solana contract, the emitter on Solana must be a signer, this
/// function helps generate a PDA and bump seed so users can emit using a PDA as the emitter.
pub fn emitter(id: &Pubkey) -> (Pubkey, Vec<&[u8]>, u8) {
let seeds = &["emitter".as_bytes()];
let (emitter, bump) = Pubkey::find_program_address(seeds, id);
(emitter, seeds.to_vec(), bump)
}
/// Deserialize helper the BridgeConfig from a Wormhole config account.
pub fn read_config(config: &AccountInfo) -> Result<BridgeConfig, WormholeError> {
let bridge_data = BridgeData::try_from_slice(&config.data.borrow())
.map_err(|_| WormholeError::DeserializeFailed)?;
Ok(bridge_data.config)
}
/// Deserialize helper for parsing from Borsh encoded VAA's from Solana accounts.
pub fn read_vaa(vaa: &AccountInfo) -> Result<PostedVAAData, WormholeError> {
Ok(PostedVAAData::try_from_slice(&vaa.data.borrow())
.map_err(|_| WormholeError::DeserializeFailed)?)
}
/// This helper method wraps the steps required to invoke Wormhole, it takes care of fee payment,
/// emitter derivation, and function invocation. This will be the right thing to use if you need to
/// simply emit a message in the most straight forward way possible.
pub fn post_message(
program_id: Pubkey,
payer: Pubkey,
message: Pubkey,
payload: impl AsRef<[u8]>,
consistency: ConsistencyLevel,
seeds: Option<&[&[u8]]>,
accounts: &[AccountInfo],
nonce: u32,
) -> ProgramResult {
// Derive any necessary Pubkeys, derivation makes sure that we match the accounts the are being
// provided by the user as well.
let id = id();
let fee_collector = fee_collector(&id);
let (emitter, mut emitter_seeds, bump) = emitter(&program_id);
let bump = &[bump];
emitter_seeds.push(bump);
// Filter for the Config AccountInfo so we can access its data.
let config = config(&id);
let config = accounts.iter().find(|item| *item.key == config).unwrap();
let config = read_config(config).unwrap();
// Pay Fee to the Wormhole
invoke_signed(
&solana_program::system_instruction::transfer(
&payer,
&fee_collector,
config.fee
),
accounts,
&[],
)?;
// Invoke the Wormhole post_message endpoint to create an on-chain message.
invoke_signed(
&instructions::post_message(
id,
payer,
emitter,
message,
nonce,
payload.as_ref().to_vec(),
consistency,
)
.unwrap(),
accounts,
&[&emitter_seeds, seeds.unwrap_or(&[])],
)?;
Ok(())
}

View File

@ -1,68 +0,0 @@
use cosmwasm_std::{
to_binary,
Addr,
Binary,
CosmosMsg,
DepsMut,
Env,
QueryRequest,
StdResult,
WasmMsg,
WasmQuery,
};
use serde::Serialize;
use wormhole::msg::{
ExecuteMsg,
QueryMsg,
};
use wormhole::state::ParsedVAA;
/// Export Core Mainnet Contract Address
#[cfg(feature = "mainnet")]
pub fn id() -> Addr {
Addr::unchecked("terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5")
}
/// Export Core Devnet Contract Address
#[cfg(feature = "testnet")]
pub fn id() -> Addr {
Addr::unchecked("terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v")
}
/// Export Core Devnet Contract Address
#[cfg(feature = "devnet")]
pub fn id() -> Addr {
Addr::unchecked("terra1pd65m0q9tl3v8znnz5f5ltsfegyzah7g42cx5v")
}
pub fn post_message<T>(nonce: u32, message: &T) -> StdResult<CosmosMsg>
where
T: Serialize,
T: ?Sized,
{
Ok(CosmosMsg::Wasm(WasmMsg::Execute {
contract_addr: id().to_string(),
funds: vec![],
msg: to_binary(&ExecuteMsg::PostMessage {
message: to_binary(message)?,
nonce,
})?,
}))
}
/// Parse a VAA using the Wormhole contract Query interface.
pub fn parse_vaa(
deps: DepsMut,
env: Env,
data: &Binary,
) -> StdResult<ParsedVAA> {
let vaa: ParsedVAA = deps.querier.query(&QueryRequest::Wasm(WasmQuery::Smart {
contract_addr: id().to_string(),
msg: to_binary(&QueryMsg::VerifyVAA {
vaa: data.clone(),
block_time: env.block.time.seconds(),
})?,
}))?;
Ok(vaa)
}

View File

@ -1,21 +0,0 @@
//! This SDK provides API's for implementing cross-chain message passing via the Wormhole protocol.
//! This package aims to provide a consistent API regardless of the underlying runtime, but some
//! types will differ depending on which implementation is being targeted.
//!
//! Each implementation can be toggled using feature flags, which will switch out the underlying
//! depenencies to pull in the depenendices for the corresponding runtimes.
//!
//! Implementations:
//!
//! Runtime | Feature Flag | Version
//! ----------|-------------------------|----------------------------------------------------
//! Solana | --feature=solana | solana-sdk 1.7.1
//! Terra | --feature=terra | cosmos-sdk 0.16.0
//!
//! Docs specific to each blockchain's runtime can be found in submodules within the chains module
//! at the root of this package.
pub mod chains;
pub use wormhole_core::*;
pub use chains::*;

21
sdk/scripts/copy_anchor.sh Executable file
View File

@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail
THIS=$(dirname $0)
SDK=${THIS}/..
ANCHOR=${SDK}/../anchor-contributor
# copy idl and anchor-contributor typescript file
SRC=${ANCHOR}/target
DST=${SDK}/target
mkdir -p ${DST}
cp -r ${SRC}/types ${SRC}/idl ${DST}
# copy typescript helper files
SRC=${ANCHOR}/tests/helpers
DST=${SDK}/src/anchor
mkdir -p ${DST}
cp ${SRC}/contributor.ts ${SRC}/kyc.ts ${SRC}/types.ts ${SRC}/utils.ts ${DST}

View File

@ -0,0 +1,11 @@
#!/bin/bash
set -euo pipefail
THIS=$(dirname $0)
SDK=${THIS}/..
ETHEREUM=${SDK}/../ethereum
npm run build --prefix ${ETHEREUM}
cp -r ${ETHEREUM}/build/contracts ${SDK}/evm-contracts
typechain --target=ethers-v5 --out-dir=${SDK}/src/ethers-contracts evm-contracts/*.json

145
sdk/src/evm/conductor.ts Normal file
View File

@ -0,0 +1,145 @@
import {
ChainId,
ChainName,
CHAIN_ID_SOLANA,
getEmitterAddressEth,
getForeignAssetEth,
hexToUint8Array,
parseSequenceFromLogEth,
} from "@certusone/wormhole-sdk";
import { GetSignedVAAResponse } from "@certusone/wormhole-sdk/lib/cjs/proto/publicrpc/v1/publicrpc";
import { web3 } from "@project-serum/anchor";
import { getOrCreateAssociatedTokenAccount } from "@solana/spl-token";
import { ethers } from "ethers";
import { hexToPublicKey } from "../anchor/utils";
import {
Conductor,
Conductor__factory,
IERC20__factory,
} from "../ethers-contracts";
import { AcceptedToken, ConductorSale, Raise } from "../icco";
import { bytesLikeToHex } from "../utils";
export class IccoConductor {
contract: Conductor;
chain: ChainId;
wormhole: string;
tokenBridge: string;
constructor(
address: string,
chain: ChainId,
signer: ethers.Wallet,
wormhole: string,
tokenBridge: string
) {
this.contract = Conductor__factory.connect(address, signer);
this.chain = chain;
this.wormhole = wormhole;
this.tokenBridge = tokenBridge;
}
address() {
return this.contract.address;
}
emitterAddress() {
return getEmitterAddressEth(this.address());
}
signer() {
return this.contract.signer;
}
provider() {
return this.contract.provider;
}
async createSale(
raise: Raise,
acceptedTokens: AcceptedToken[],
solanaConnection?: web3.Connection,
payer?: web3.Keypair,
custodian?: web3.PublicKey
) {
// create associated token accounts for custodian
{
for (const token of acceptedTokens) {
if (token.tokenChain == CHAIN_ID_SOLANA) {
const mint = hexToPublicKey(bytesLikeToHex(token.tokenAddress));
await getOrCreateAssociatedTokenAccount(
solanaConnection,
payer,
mint,
custodian,
true // allowOwnerOffCurve
);
}
}
}
const contract = this.contract;
// approve of spending token that exists on this chain
{
const wrapped = await getForeignAssetEth(
this.tokenBridge,
this.provider(),
raise.tokenChain,
ethers.utils.arrayify(raise.token)
);
const token = IERC20__factory.connect(wrapped, this.signer());
const receipt = await token
.approve(this.address(), raise.tokenAmount)
.then((tx) => tx.wait());
}
// now create
return contract.createSale(raise, acceptedTokens).then((tx) => tx.wait());
}
async getSale(saleId: ethers.BigNumber): Promise<ConductorSale> {
const sale = await this.contract.sales(saleId);
return {
saleId: sale.saleID,
tokenAddress: sale.tokenAddress,
tokenChain: sale.tokenChain,
localTokenDecimals: sale.localTokenDecimals,
localTokenAddress: sale.localTokenAddress,
solanaTokenAccount: sale.solanaTokenAccount,
tokenAmount: sale.tokenAmount,
minRaise: sale.minRaise,
maxRaise: sale.maxRaise,
saleStart: sale.saleStart,
saleEnd: sale.saleEnd,
initiator: sale.initiator,
recipient: sale.recipient,
refundRecipient: sale.refundRecipient,
acceptedTokensChains: sale.acceptedTokensChains,
acceptedTokensAddresses: sale.acceptedTokensAddresses,
acceptedTokensConversionRates: sale.acceptedTokensConversionRates,
solanaAcceptedTokensCount: sale.solanaAcceptedTokensCount,
contributions: sale.contributions,
contributionsCollected: sale.contributionsCollected,
isSealed: sale.isSealed,
isAborted: sale.isAborted,
};
}
async collectContribution(signedVaa: Uint8Array) {
return this.contract.collectContribution(signedVaa).then((tx) => tx.wait());
}
async sealSale(saleId: ethers.BigNumber) {
// save on gas by checking the state of the sale
const sale = await this.getSale(saleId);
if (sale.isSealed || sale.isAborted) {
throw Error("already sealed / aborted");
}
// and seal
return this.contract.sealSale(saleId).then((tx) => tx.wait());
}
}

View File

@ -0,0 +1,27 @@
import { ethers } from "ethers";
import { Contributor__factory } from "../ethers-contracts";
import { getExcessContributionIsClaimedOnEth } from "./getters";
export async function claimExcessContributionOnEth(
contributorAddress: string,
saleId: ethers.BigNumberish,
tokenIndex: number,
wallet: ethers.Wallet
): Promise<ethers.ContractReceipt> {
const contributor = Contributor__factory.connect(contributorAddress, wallet);
const isClaimed = await getExcessContributionIsClaimedOnEth(
contributorAddress,
wallet.provider,
saleId,
tokenIndex,
wallet.address
);
if (isClaimed) {
throw Error("excessContribution already claimed");
}
const tx = await contributor.claimExcessContribution(saleId, tokenIndex);
return tx.wait();
}

View File

@ -1,6 +1,6 @@
import { ethers } from "ethers";
import { Conductor__factory, Contributor__factory } from "../ethers-contracts";
import { Contributor__factory } from "../ethers-contracts";
import { getRefundIsClaimedOnEth } from "./getters";
export async function claimContributorRefundOnEth(
@ -25,14 +25,3 @@ export async function claimContributorRefundOnEth(
const tx = await contributor.claimRefund(saleId, tokenIndex);
return tx.wait();
}
export async function claimConductorRefundOnEth(
conductorAddress: string,
saleId: ethers.BigNumberish,
wallet: ethers.Wallet
): Promise<ethers.ContractReceipt> {
const conductor = Conductor__factory.connect(conductorAddress, wallet);
const tx = await conductor.claimRefund(saleId);
return tx.wait();
}

View File

@ -1,9 +1,5 @@
import { ethers } from "ethers";
import {
ChainId,
ERC20__factory,
getForeignAssetEth,
} from "@certusone/wormhole-sdk";
import { ChainId, ERC20__factory, getForeignAssetEth } from "@certusone/wormhole-sdk";
import { nativeToUint8Array } from "./misc";
import { Conductor__factory } from "../ethers-contracts";
import { AcceptedToken, SaleInit, makeAcceptedToken, Raise } from "./structs";
@ -23,12 +19,7 @@ export async function makeAcceptedWrappedTokenEth(
}
const originAsset = nativeToUint8Array(originTokenAddress, originChainId);
const foreignTokenAddress = await getForeignAssetEth(
tokenBridgeAddress,
provider,
originChainId,
originAsset
);
const foreignTokenAddress = await getForeignAssetEth(tokenBridgeAddress, provider, originChainId, originAsset);
if (foreignTokenAddress === null) {
throw Error("cannot find foreign asset");
}
@ -38,6 +29,7 @@ export async function makeAcceptedWrappedTokenEth(
export async function createSaleOnEth(
conductorAddress: string,
isFixedPrice: boolean,
localTokenAddress: string,
tokenAddress: string,
tokenChain: ChainId,
@ -46,10 +38,11 @@ export async function createSaleOnEth(
maxRaise: ethers.BigNumberish,
saleStart: ethers.BigNumberish,
saleEnd: ethers.BigNumberish,
unlockTimestamp: ethers.BigNumberish,
acceptedTokens: AcceptedToken[],
solanaTokenAccount: ethers.BytesLike,
recipientAddress: string,
refundRecipientAddress: string,
authority: string, // kyc
wallet: ethers.Wallet
): Promise<ethers.ContractReceipt> {
// approve first
@ -64,6 +57,7 @@ export async function createSaleOnEth(
// create a struct to pass to createSale
const raise: Raise = {
isFixedPrice: isFixedPrice,
token: tokenAddressBytes32,
tokenChain: tokenChain,
tokenAmount: amount,
@ -71,9 +65,10 @@ export async function createSaleOnEth(
maxRaise: maxRaise,
saleStart: ethers.BigNumber.from(saleStart),
saleEnd: ethers.BigNumber.from(saleEnd),
unlockTimestamp: ethers.BigNumber.from(unlockTimestamp),
recipient: recipientAddress,
refundRecipient: refundRecipientAddress,
solanaTokenAccount: solanaTokenAccount,
authority: authority,
};
// now create

View File

@ -37,7 +37,7 @@ export async function getSaleFromConductorOnEth(
contributionsCollected: sale.contributionsCollected,
isSealed: sale.isSealed,
isAborted: sale.isAborted,
refundIsClaimed: sale.refundIsClaimed,
authority: sale.authority,
};
}
@ -46,10 +46,7 @@ export async function getSaleFromContributorOnEth(
provider: ethers.providers.Provider,
saleId: ethers.BigNumberish
): Promise<ContributorSale> {
const contributor = Contributor__factory.connect(
contributorAddress,
provider
);
const contributor = Contributor__factory.connect(contributorAddress, provider);
const sale = await contributor.sales(saleId);
@ -57,22 +54,19 @@ export async function getSaleFromContributorOnEth(
saleId: sale.saleID,
tokenAddress: sale.tokenAddress,
tokenChain: sale.tokenChain,
tokenAmount: sale.tokenAmount,
tokenDecimals: sale.tokenDecimals,
minRaise: sale.minRaise,
maxRaise: sale.maxRaise,
saleStart: sale.saleStart,
saleEnd: sale.saleEnd,
recipient: sale.recipient,
refundRecipient: sale.refundRecipient,
acceptedTokensChains: sale.acceptedTokensChains,
acceptedTokensAddresses: sale.acceptedTokensAddresses,
acceptedTokensConversionRates: sale.acceptedTokensConversionRates,
solanaTokenAccount: sale.solanaTokenAccount,
disabledAcceptedTokens: sale.disabledAcceptedTokens,
isSealed: sale.isSealed,
isAborted: sale.isAborted,
allocations: sale.allocations,
excessContributions: sale.excessContributions,
authority: sale.authority,
};
}
@ -83,13 +77,31 @@ export async function getAllocationIsClaimedOnEth(
tokenIndex: number,
walletAddress: string
): Promise<boolean> {
const contributor = Contributor__factory.connect(
contributorAddress,
provider
);
const contributor = Contributor__factory.connect(contributorAddress, provider);
return contributor.allocationIsClaimed(saleId, tokenIndex, walletAddress);
}
export async function getExcessContributionIsClaimedOnEth(
contributorAddress: string,
provider: ethers.providers.Provider,
saleId: ethers.BigNumberish,
tokenIndex: number,
walletAddress: string
): Promise<boolean> {
const contributor = Contributor__factory.connect(contributorAddress, provider);
return contributor.excessContributionIsClaimed(saleId, tokenIndex, walletAddress);
}
export async function getSaleExcessContributionOnEth(
contributorAddress: string,
provider: ethers.providers.Provider,
saleId: ethers.BigNumberish,
tokenIndex: number
): Promise<ethers.BigNumberish> {
const contributor = Contributor__factory.connect(contributorAddress, provider);
return contributor.getSaleExcessContribution(saleId, tokenIndex);
}
export async function getContributorContractOnEth(
conductorAddress: string,
provider: ethers.providers.Provider,
@ -107,10 +119,7 @@ export async function getRefundIsClaimedOnEth(
tokenIndex: number,
walletAddress: string
): Promise<boolean> {
const contributor = Contributor__factory.connect(
contributorAddress,
provider
);
const contributor = Contributor__factory.connect(contributorAddress, provider);
return contributor.refundIsClaimed(saleId, tokenIndex, walletAddress);
}
@ -120,10 +129,7 @@ export async function getSaleTotalContributionOnEth(
saleId: ethers.BigNumberish,
tokenIndex: number
): Promise<ethers.BigNumber> {
const contributor = Contributor__factory.connect(
contributorAddress,
provider
);
const contributor = Contributor__factory.connect(contributorAddress, provider);
return contributor.getSaleTotalContribution(saleId, tokenIndex);
}
@ -134,10 +140,7 @@ export async function getSaleContributionOnEth(
tokenIndex: number,
walletAddress: string
): Promise<ethers.BigNumber> {
const contributor = Contributor__factory.connect(
contributorAddress,
provider
);
const contributor = Contributor__factory.connect(contributorAddress, provider);
return contributor.getSaleContribution(saleId, tokenIndex, walletAddress);
}
@ -147,10 +150,7 @@ export async function getSaleAllocationOnEth(
saleId: ethers.BigNumberish,
tokenIndex: number
): Promise<ethers.BigNumber> {
const contributor = Contributor__factory.connect(
contributorAddress,
provider
);
const contributor = Contributor__factory.connect(contributorAddress, provider);
return contributor.getSaleAllocation(saleId, tokenIndex);
}
@ -161,23 +161,10 @@ export async function getSaleWalletAllocationOnEth(
tokenIndex: number,
walletAddress: string
): Promise<ethers.BigNumber> {
const [allocation, walletContribution, totalContribution] = await Promise.all(
[
getSaleAllocationOnEth(contributorAddress, provider, saleId, tokenIndex),
getSaleContributionOnEth(
contributorAddress,
provider,
saleId,
tokenIndex,
walletAddress
),
getSaleTotalContributionOnEth(
contributorAddress,
provider,
saleId,
tokenIndex
),
]
);
const [allocation, walletContribution, totalContribution] = await Promise.all([
getSaleAllocationOnEth(contributorAddress, provider, saleId, tokenIndex),
getSaleContributionOnEth(contributorAddress, provider, saleId, tokenIndex, walletAddress),
getSaleTotalContributionOnEth(contributorAddress, provider, saleId, tokenIndex),
]);
return allocation.mul(walletContribution).div(totalContribution);
}

View File

@ -1,6 +1,7 @@
export * from "./abortSaleBeforeStartTime";
export * from "./attestContributions";
export * from "./claimAllocation";
export * from "./claimExcessContribution";
export * from "./claimRefund";
export * from "./collectContribution";
export * from "./contribute";
@ -14,3 +15,4 @@ export * from "./saleSealed";
export * from "./sealSale";
export * from "./signedVaa";
export * from "./structs";
export * from "./saleAuthorityUpdated";

54
sdk/src/icco/misc.ts Normal file
View File

@ -0,0 +1,54 @@
import { ethers } from "ethers";
import {
ChainId,
ERC20__factory,
IWETH__factory,
getForeignAssetEth,
getOriginalAssetEth,
tryNativeToUint8Array,
tryUint8ArrayToNative,
} from "@certusone/wormhole-sdk";
import { parseUnits } from "ethers/lib/utils";
export { tryNativeToUint8Array as nativeToUint8Array };
export async function wrapEth(wethAddress: string, amount: string, wallet: ethers.Wallet): Promise<void> {
const weth = IWETH__factory.connect(wethAddress, wallet);
await weth.deposit({
value: ethers.utils.parseUnits(amount),
});
}
export async function getCurrentBlock(provider: ethers.providers.Provider): Promise<ethers.providers.Block> {
const currentBlockNumber = await provider.getBlockNumber();
return provider.getBlock(currentBlockNumber);
}
export async function sleepFor(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export async function getErc20Balance(
provider: ethers.providers.Provider,
tokenAddress: string,
walletAddress: string
): Promise<ethers.BigNumber> {
const token = ERC20__factory.connect(tokenAddress, provider);
return token.balanceOf(walletAddress);
}
export async function getErc20Decimals(provider: ethers.providers.Provider, tokenAddress: string): Promise<number> {
const token = ERC20__factory.connect(tokenAddress, provider);
return token.decimals();
}
export async function normalizeConversionRate(
denominationDecimals: number,
acceptedTokenDecimals: number,
conversionRate: string
): Promise<ethers.BigNumberish> {
const precision = 18;
const normDecimals = denominationDecimals + precision - acceptedTokenDecimals;
let normalizedConversionRate = parseUnits(conversionRate, normDecimals);
return normalizedConversionRate;
}

View File

@ -0,0 +1,13 @@
import { ethers } from "ethers";
import { Contributor__factory } from "../ethers-contracts";
export async function saleAuthorityUpdatedOnEth(
contributorAddress: string,
signedVaa: Uint8Array,
wallet: ethers.Wallet
): Promise<ethers.ContractReceipt> {
const contributor = Contributor__factory.connect(contributorAddress, wallet);
const tx = await contributor.saleAuthorityUpdated(signedVaa);
return tx.wait();
}

122
sdk/src/icco/signedVaa.ts Normal file
View File

@ -0,0 +1,122 @@
import { ethers } from "ethers";
import { ChainId, uint8ArrayToHex } from "@certusone/wormhole-sdk";
import { AcceptedToken, Allocation, SaleInit, SolanaSaleInit, SolanaToken, SaleSealed } from "./structs";
const VAA_PAYLOAD_NUM_ACCEPTED_TOKENS = 132;
const VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH = 50;
export async function getSaleIdFromIccoVaa(payload: Uint8Array): Promise<ethers.BigNumberish> {
return ethers.BigNumber.from(payload.slice(1, 33)).toString();
}
export async function getTargetChainIdFromTransferVaa(payload: Uint8Array): Promise<ChainId> {
return Buffer.from(payload).readUInt16BE(99) as ChainId;
}
export async function parseSaleInit(payload: Uint8Array): Promise<SaleInit> {
const buffer = Buffer.from(payload);
const numAcceptedTokens = buffer.readUInt8(VAA_PAYLOAD_NUM_ACCEPTED_TOKENS);
const recipientIndex =
VAA_PAYLOAD_NUM_ACCEPTED_TOKENS + numAcceptedTokens * VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH + 1;
return {
payloadId: buffer.readUInt8(0),
saleId: ethers.BigNumber.from(payload.slice(1, 33)).toString(),
tokenAddress: uint8ArrayToHex(payload.slice(33, 65)),
tokenChain: buffer.readUInt16BE(65),
tokenDecimals: buffer.readUInt8(67),
saleStart: ethers.BigNumber.from(payload.slice(68, 100)).toString(),
saleEnd: ethers.BigNumber.from(payload.slice(100, 132)).toString(),
acceptedTokens: parseAcceptedTokens(payload, numAcceptedTokens),
recipient: uint8ArrayToHex(payload.slice(recipientIndex, recipientIndex + 32)),
authority: uint8ArrayToHex(payload.slice(recipientIndex + 32, recipientIndex + 52)),
unlockTimestamp: ethers.BigNumber.from(payload.slice(recipientIndex + 52, recipientIndex + 84)).toString(),
};
}
function parseAcceptedTokens(payload: Uint8Array, numTokens: number): AcceptedToken[] {
const buffer = Buffer.from(payload);
const tokens: AcceptedToken[] = [];
for (let i = 0; i < numTokens; ++i) {
const startIndex = VAA_PAYLOAD_NUM_ACCEPTED_TOKENS + 1 + i * VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH;
const token: AcceptedToken = {
tokenAddress: uint8ArrayToHex(payload.slice(startIndex, startIndex + 32)),
tokenChain: buffer.readUInt16BE(startIndex + 32),
conversionRate: ethers.BigNumber.from(
payload.slice(startIndex + 34, startIndex + VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH)
).toString(),
};
tokens.push(token);
}
return tokens;
}
const SOLANA_VAA_PAYLOAD_NUM_ACCEPTED_TOKENS = 132;
const SOLANA_VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH = 33;
export async function parseSolanaSaleInit(payload: Uint8Array): Promise<SolanaSaleInit> {
const buffer = Buffer.from(payload);
const numAcceptedTokens = buffer.readUInt8(SOLANA_VAA_PAYLOAD_NUM_ACCEPTED_TOKENS);
const recipientIndex =
SOLANA_VAA_PAYLOAD_NUM_ACCEPTED_TOKENS + numAcceptedTokens * SOLANA_VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH + 1;
return {
payloadId: buffer.readUInt8(0),
saleId: ethers.BigNumber.from(payload.slice(1, 33)).toString(),
tokenAddress: uint8ArrayToHex(payload.slice(33, 65)),
tokenChain: buffer.readUInt16BE(65),
tokenDecimals: buffer.readUInt8(67),
saleStart: ethers.BigNumber.from(payload.slice(68, 100)).toString(),
saleEnd: ethers.BigNumber.from(payload.slice(100, 132)).toString(),
acceptedTokens: parseSolanaAcceptedTokens(payload, numAcceptedTokens),
recipient: uint8ArrayToHex(payload.slice(recipientIndex, recipientIndex + 32)),
};
}
function parseSolanaAcceptedTokens(payload: Uint8Array, numTokens: number): SolanaToken[] {
const buffer = Buffer.from(payload);
const tokens: SolanaToken[] = [];
for (let i = 0; i < numTokens; ++i) {
const startIndex = SOLANA_VAA_PAYLOAD_NUM_ACCEPTED_TOKENS + 1 + i * SOLANA_VAA_PAYLOAD_ACCEPTED_TOKEN_BYTES_LENGTH;
const token: SolanaToken = {
tokenIndex: buffer.readUInt8(startIndex),
tokenAddress: uint8ArrayToHex(payload.slice(startIndex + 1, startIndex + 33)),
};
tokens.push(token);
}
return tokens;
}
const VAA_PAYLOAD_NUM_ALLOCATIONS = 33;
const VAA_PAYLOAD_ALLOCATION_BYTES_LENGTH = 65;
export async function parseSaleSealed(payload: Uint8Array): Promise<SaleSealed> {
const buffer = Buffer.from(payload);
const numAllocations = buffer.readUInt8(VAA_PAYLOAD_NUM_ALLOCATIONS);
return {
payloadId: buffer.readUInt8(0),
saleId: ethers.BigNumber.from(payload.slice(1, 33)).toString(),
allocations: parseAllocations(payload, numAllocations),
};
}
function parseAllocations(payload: Uint8Array, numAllocations: number): Allocation[] {
const buffer = Buffer.from(payload);
const allocations: Allocation[] = [];
for (let i = 0; i < numAllocations; ++i) {
const startIndex = VAA_PAYLOAD_NUM_ALLOCATIONS + 1 + i * VAA_PAYLOAD_ALLOCATION_BYTES_LENGTH;
const allocation: Allocation = {
tokenIndex: buffer.readUInt8(startIndex),
allocation: ethers.BigNumber.from(payload.slice(startIndex + 1, startIndex + 33)).toString(),
excessContribution: ethers.BigNumber.from(payload.slice(startIndex + 33, startIndex + 65)).toString(),
};
allocations.push(allocation);
}
return allocations;
}

View File

@ -4,6 +4,7 @@ import { ChainId } from "@certusone/wormhole-sdk";
import { nativeToUint8Array } from "./misc";
export interface Raise {
isFixedPrice: boolean;
token: ethers.BytesLike;
tokenChain: ChainId;
tokenAmount: ethers.BigNumberish;
@ -11,9 +12,10 @@ export interface Raise {
maxRaise: ethers.BigNumberish;
saleStart: ethers.BigNumberish;
saleEnd: ethers.BigNumberish;
unlockTimestamp: ethers.BigNumberish;
recipient: string;
refundRecipient: string;
solanaTokenAccount: ethers.BytesLike;
authority: string;
}
export interface Sale {
@ -35,6 +37,8 @@ export interface Sale {
// state
isSealed: boolean;
isAborted: boolean;
// kyc authority
authority: string;
}
export interface ConductorSale extends Sale {
@ -45,14 +49,30 @@ export interface ConductorSale extends Sale {
solanaAcceptedTokensCount: number;
contributions: ethers.BigNumberish[];
contributionsCollected: boolean[];
refundIsClaimed: boolean;
}
export interface ContributorSale extends Sale {
export interface ContributorSale {
// sale init
saleId: ethers.BigNumberish;
tokenAddress: ethers.BytesLike;
tokenChain: number;
saleStart: ethers.BigNumberish;
saleEnd: ethers.BigNumberish;
recipient: ethers.BytesLike;
// accepted tokens
acceptedTokensChains: number[];
acceptedTokensAddresses: ethers.BytesLike[];
acceptedTokensConversionRates: ethers.BigNumberish[];
disabledAcceptedTokens: boolean[];
// state
isSealed: boolean;
isAborted: boolean;
// yep
tokenDecimals: number;
solanaTokenAccount: ethers.BytesLike;
allocations: ethers.BigNumberish[];
excessContributions: ethers.BigNumberish[];
// authority
authority: string;
}
export interface AcceptedToken {
@ -67,15 +87,12 @@ export interface SaleInit {
tokenAddress: string;
tokenChain: number;
tokenDecimals: number;
tokenAmount: ethers.BigNumberish;
minRaise: ethers.BigNumberish;
maxRaise: ethers.BigNumberish;
saleStart: ethers.BigNumberish;
saleEnd: ethers.BigNumberish;
acceptedTokens: AcceptedToken[];
solanaTokenAccount: ethers.BytesLike;
recipient: string;
refundRecipient: string;
authority: string;
unlockTimestamp: ethers.BigNumberish;
}
export interface SolanaToken {
@ -86,7 +103,7 @@ export interface SolanaToken {
export interface SolanaSaleInit {
payloadId: number;
saleId: ethers.BigNumberish;
solanaTokenAccount: ethers.BytesLike;
tokenAddress: ethers.BytesLike;
tokenChain: number;
tokenDecimals: number;
saleStart: ethers.BigNumberish;
@ -107,11 +124,7 @@ export interface SaleSealed {
allocations: Allocation[];
}
export function makeAcceptedToken(
chainId: ChainId,
address: string,
conversion: ethers.BigNumberish
): AcceptedToken {
export function makeAcceptedToken(chainId: ChainId, address: string, conversion: ethers.BigNumberish): AcceptedToken {
return {
tokenChain: chainId,
tokenAddress: nativeToUint8Array(address, chainId),

View File

@ -0,0 +1,16 @@
import { ethers } from "ethers";
import { Conductor__factory } from "../ethers-contracts";
export async function updateSaleAuthorityOnEth(
conductorAddress: string,
wallet: ethers.Wallet,
saleId: ethers.BigNumberish,
newAuthority: string,
signature: ethers.BytesLike,
): Promise<ethers.ContractReceipt> {
const conductor = Conductor__factory.connect(conductorAddress, wallet);
// and seal
const tx = await conductor.updateSaleAuthority(saleId, newAuthority, signature);
return tx.wait();
}

84
sdk/src/testnet/consts.ts Normal file
View File

@ -0,0 +1,84 @@
const fs = require("fs");
import { web3 } from "@project-serum/anchor";
import { ChainId, CHAIN_ID_ETH, CHAIN_ID_AVAX, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
export const REPO_PATH = `${__dirname}/../../..`;
export const SDK_PATH = `${REPO_PATH}/sdk`;
export const WORMHOLE_ADDRESSES = {
guardianRpc: ["https://wormhole-v2-testnet-api.certus.one"],
solana_devnet: {
wormhole: "3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5",
tokenBridge: "DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe",
chainId: 1,
},
goerli: {
wormhole: "0x706abc4E45D419950511e474C7B9Ed348A4a716c",
tokenBridge: "0xF890982f9310df57d00f659cf4fd87e65adEd8d7",
chainId: 2,
},
fuji: {
wormhole: "0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C",
tokenBridge: "0x61E44E506Ca5659E6c0bba9b678586fA2d729756",
chainId: 6,
},
binance_testnet: {
wormhole: "0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D",
tokenBridge: "0x9dcF9D205C9De35334D646BeE44b2D2859712A09",
chainId: 4,
},
mumbai: {
wormhole: "0x0CBE91CF822c73C2315FB05100C2F714765d5c20",
tokenBridge: "0x377D55a7928c046E18eEbb61977e714d2a76472a",
chainId: 5,
},
fantom_testnet: {
wormhole: "0x1BB3B4119b7BA9dfad76B0545fb3F531383c3bB7",
tokenBridge: "0x599CEa2204B4FaECd584Ab1F2b6aCA137a0afbE8",
chainId: 10,
},
};
const REPO_ROOT = `${__dirname}/../../..`;
const TESTNET_CFG = `${REPO_ROOT}/sdk/cfg/testnet`;
export const TESTNET_ADDRESSES = JSON.parse(fs.readFileSync(`${REPO_ROOT}/testnet.json`, "utf8"));
export const SALE_CONFIG = JSON.parse(fs.readFileSync(`${TESTNET_CFG}/saleConfig.json`, "utf8"));
export const CONTRIBUTOR_INFO = JSON.parse(fs.readFileSync(`${TESTNET_CFG}/contributors.json`, "utf8"));
//export const SOLANA_IDL = JSON.parse(fs.readFileSync(`${__dirname}/../solana/anchor_contributor.json`, "utf8"));
// VAA fetching params
export const RETRY_TIMEOUT_SECONDS = 180;
// deployment info for the sale
export const CONDUCTOR_ADDRESS = TESTNET_ADDRESSES.conductorAddress;
export const CONDUCTOR_CHAIN_ID = TESTNET_ADDRESSES.conductorChain;
export const CONDUCTOR_NETWORK = SALE_CONFIG["conductorNetwork"];
export const KYC_AUTHORITY_KEY = SALE_CONFIG["authority"];
// will switch to this key when testing updateSaleAuthority (this is the testnet guardian key)
export const NEW_KYC_AUTHORITY_KEY = "cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0";
export const CONTRIBUTOR_NETWORKS: string[] = ["goerli", "fuji"];
// add chainId to network map for the contributors
export const CHAIN_ID_TO_NETWORK = new Map<ChainId, string>();
CHAIN_ID_TO_NETWORK.set(CHAIN_ID_ETH, CONTRIBUTOR_NETWORKS[0]);
CHAIN_ID_TO_NETWORK.set(CHAIN_ID_AVAX, CONTRIBUTOR_NETWORKS[1]);
// conductor
export const CONDUCTOR_NATIVE_CHAIN = CHAIN_ID_AVAX;
export const CONDUCTOR_NATIVE_ADDRESS = "0xe9B4337f3ec72c6EAa519475E54CB2ba7621A7e0";
// kyc
export const KYC_AUTHORITY = "0x1dF62f291b2E969fB0849d99D9Ce41e2F137006e";
export const KYC_PRIVATE = "b0057716d5917badaf911b193b12b910811c1497b5bada8d7711f758981c3773";
// guardians
export const WORMHOLE_RPCS = ["https://wormhole-v2-testnet-api.certus.one"];
// solana
export const SOLANA_RPC = "https://api.devnet.solana.com";
export const SOLANA_CORE_BRIDGE_ADDRESS = new web3.PublicKey("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5");
export const SOLANA_TOKEN_BRIDGE_ADDRESS = new web3.PublicKey("DZnkkTmCiFWfYTfT41X3Rd1kDgozqzxWaHqsw6W4x2oe");
export const SOLANA_CONTRIBUTOR_ADDRESS = new web3.PublicKey("NEXaa1zDNLJ9AqwEd7LipQTge4ygeVVHyr8Tv7X2FCn");
// avax
export const WAVAX_ADDRESS = "0xd00ae08403B9bbb9124bB305C09058E32C39A48c";
export const AVAX_CORE_BRIDGE_ADDRESS = "0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C";
export const AVAX_TOKEN_BRIDGE_ADDRESS = "0x61E44E506Ca5659E6c0bba9b678586fA2d729756";

13
sdk/src/testnet/io.ts Normal file
View File

@ -0,0 +1,13 @@
import { web3 } from "@project-serum/anchor";
import fs from "fs";
export function readJson(filename: string): any {
if (!fs.existsSync(filename)) {
throw Error(`${filename} does not exist`);
}
return JSON.parse(fs.readFileSync(filename, "utf8"));
}
export function readKeypair(filename: string): web3.Keypair {
return web3.Keypair.fromSecretKey(Uint8Array.from(readJson(filename)));
}

70
sdk/src/testnet/kyc.ts Normal file
View File

@ -0,0 +1,70 @@
import { tryNativeToHexString } from "@certusone/wormhole-sdk";
import { ethers } from "ethers";
import { soliditySha3 } from "web3-utils";
const elliptic = require("elliptic");
export function signNewAuthorityOnEth(
conductorAddress: string,
saleId: ethers.BigNumberish,
signer: string
): Buffer {
const body = Buffer.alloc(2 * 32, 0);
body.write(conductorAddress, 0, "hex");
body.write(toBigNumberHex(saleId.toString(), 32), 32, "hex");
const hash = soliditySha3("0x" + body.toString("hex"));
if (hash == null) {
throw "hash == null";
}
const ec = new elliptic.ec("secp256k1");
const key = ec.keyFromPrivate(signer);
const signature = key.sign(hash.substring(2), { canonical: true });
const packed = Buffer.alloc(65);
packed.write(signature.r.toString(16).padStart(64, "0"), 0, "hex");
packed.write(signature.s.toString(16).padStart(64, "0"), 32, "hex");
packed.writeUInt8(signature.recoveryParam, 64);
return packed;
}
export function signContributionOnEth(
conductorAddress: string,
saleId: ethers.BigNumberish,
tokenIndex: number,
amount: ethers.BigNumberish,
buyerAddress: string,
totalContribution: ethers.BigNumberish,
signer: string
): Buffer {
const body = Buffer.alloc(6 * 32, 0);
body.write(conductorAddress, 0, "hex");
body.write(toBigNumberHex(saleId.toString(), 32), 32, "hex");
body.write(toBigNumberHex(tokenIndex, 32), 2 * 32, "hex");
body.write(toBigNumberHex(amount.toString(), 32), 3 * 32, "hex");
body.write(tryNativeToHexString(buyerAddress, "ethereum"), 4 * 32, "hex");
body.write(toBigNumberHex(totalContribution.toString(), 32), 5 * 32, "hex");
const hash = soliditySha3("0x" + body.toString("hex"));
if (hash == null) {
throw "hash == null";
}
const ec = new elliptic.ec("secp256k1");
const key = ec.keyFromPrivate(signer);
const signature = key.sign(hash.substring(2), { canonical: true });
const packed = Buffer.alloc(65);
packed.write(signature.r.toString(16).padStart(64, "0"), 0, "hex");
packed.write(signature.s.toString(16).padStart(64, "0"), 32, "hex");
packed.writeUInt8(signature.recoveryParam, 64);
return packed;
}
export function toBigNumberHex(value: ethers.BigNumberish, numBytes: number): string {
return ethers.BigNumber.from(value)
.toHexString()
.substring(2)
.padStart(numBytes * 2, "0");
}

76
sdk/src/testnet/purse.ts Normal file
View File

@ -0,0 +1,76 @@
import { web3 } from "@project-serum/anchor";
import { ethers } from "ethers";
import { ChainId, CHAIN_ID_AVAX, CHAIN_ID_ETH } from "@certusone/wormhole-sdk";
import { readJson } from "./io";
interface EvmWallets {
endpoint: string;
provider: ethers.providers.Provider;
wallets: ethers.Wallet[];
}
interface SolanaWallets {
endpoint: string;
provider: web3.Connection;
wallets: web3.Keypair[];
}
export class Purse {
avax: EvmWallets;
ethereum: EvmWallets;
solana: SolanaWallets;
constructor(filename: string) {
const cfg = readJson(filename);
this.avax = {
endpoint: cfg.avax.rpc,
provider: new ethers.providers.StaticJsonRpcProvider(cfg.avax.rpc),
wallets: [],
};
this.ethereum = {
endpoint: cfg.ethereum.rpc,
provider: new ethers.providers.StaticJsonRpcProvider(cfg.ethereum.rpc),
wallets: [],
};
this.solana = {
endpoint: cfg.solana.rpc,
provider: new web3.Connection(cfg.solana.rpc, "finalized"),
wallets: [],
};
// solana devnet
for (const key of cfg.solana.wallets) {
this.solana.wallets.push(web3.Keypair.fromSecretKey(Uint8Array.from(key)));
}
// fuji
for (const pk of cfg.avax.wallets) {
this.avax.wallets.push(new ethers.Wallet(pk, this.avax.provider));
}
// goerli
for (const pk of cfg.ethereum.wallets) {
this.ethereum.wallets.push(new ethers.Wallet(pk, this.ethereum.provider));
}
}
getEvmWallet(chainId: ChainId, index: number) {
switch (chainId) {
case CHAIN_ID_ETH: {
return this.ethereum.wallets.at(index);
}
case CHAIN_ID_AVAX: {
return this.avax.wallets.at(index);
}
default: {
throw Error("unrecognized chainId");
}
}
}
getSolanaWallet(index: number) {
return this.solana.wallets.at(index);
}
}

View File

@ -0,0 +1,393 @@
import { assert, expect } from "chai";
import {
initiatorWallet,
buildAcceptedTokens,
createSaleOnEthConductor,
initializeSaleOnEthContributors,
waitForSaleToStart,
prepareAndExecuteContribution,
waitForSaleToEnd,
sealOrAbortSaleOnEth,
sealSaleAtEthContributors,
redeemCrossChainAllocations,
claimContributorAllocationOnEth,
redeemCrossChainContributions,
testProvider,
collectContributionsOnConductor,
attestContributionsOnContributor,
getSaleTokenBalancesOnContributors,
findUniqueContributions,
excessContributionsExistForSale,
claimContributorExcessContributionOnEth,
getContributedTokenBalancesOnContributors,
getTokenDecimals,
abortSaleAtContributors,
claimRefundForContributorOnEth,
updateSaleAuthorityOnConductor,
authorityUpdatedOnEthContributors,
} from "./utils";
import {
SALE_CONFIG,
TESTNET_ADDRESSES,
CONDUCTOR_NETWORK,
CONTRIBUTOR_INFO,
CONTRIBUTOR_NETWORKS,
WORMHOLE_ADDRESSES,
CONDUCTOR_CHAIN_ID,
CONDUCTOR_ADDRESS,
CHAIN_ID_TO_NETWORK,
} from "./consts";
import { Contribution, SaleParams, SealSaleResult } from "./structs";
import { setDefaultWasm, ChainId, tryUint8ArrayToNative, tryNativeToUint8Array } from "@certusone/wormhole-sdk";
import { MockSale } from "./testCalculator";
import { getErc20Balance, makeAcceptedToken, getSaleFromContributorOnEth } from "../";
import { ethers } from "ethers";
setDefaultWasm("node");
describe("Testnet ICCO Successful Sales", () => {
// read in test configs
const raiseParams: SaleParams = SALE_CONFIG["raiseParams"];
const contributions: Contribution[] = CONTRIBUTOR_INFO["contributions"];
it("Successful Fixed-price With Lock Up", async () => {
// this test will handle successful sales and oversubscribed sales
// successful sale: minRaise < totalRaised < maxRaise
// oversubscribed sale: totalRaised > maxRaise >= minRaise
// sale parameters
const acceptedTokens = await buildAcceptedTokens(SALE_CONFIG["acceptedTokens"]);
// test calculator object
const mockSale = new MockSale(
CONDUCTOR_CHAIN_ID,
SALE_CONFIG["denominationDecimals"],
acceptedTokens,
raiseParams,
contributions
);
const mockSaleResults = await mockSale.getResults();
// create and initialize the sale
const saleInitArray = await createSaleOnEthConductor(
initiatorWallet(CONDUCTOR_NETWORK),
TESTNET_ADDRESSES.conductorAddress,
raiseParams,
acceptedTokens
);
// // initialize the sale on the contributors
const saleInit = await initializeSaleOnEthContributors(saleInitArray[0]);
console.log(saleInit);
console.info("Sale", saleInit.saleId, "has been initialized on the EVM contributors.");
// wait for the sale to start before contributing
console.info("Waiting for the sale to start.");
const extraTime: number = 5; // wait an extra 5 seconds
await waitForSaleToStart(saleInit, extraTime);
// loop through contributors and safe contribute one by one
console.log("Making contributions to the sale.");
for (const contribution of contributions) {
let successful = false;
// check if we're contributing a solana token
successful = await prepareAndExecuteContribution(saleInit.saleId, raiseParams.token, contribution);
expect(successful, "Contribution failed").to.be.true;
}
// wait for sale to end
console.log("Waiting for the sale to end.");
await waitForSaleToEnd(saleInit, raiseParams.lockUpDurationSeconds); // add the lock up duration
// attest and collect contributions on EVM
const attestVaas: Uint8Array[] = await attestContributionsOnContributor(saleInit);
console.log("Successfully attested contributions on", attestVaas.length, "chains.");
// collect contributions on the conductor
const collectionResults = await collectContributionsOnConductor(attestVaas, saleInit.saleId);
for (const result of collectionResults) {
expect(result, "Failed to collect all contributions on the conductor.").to.be.true;
}
console.log("Successfully collected contributions on the conductor.");
// seal the sale on the conductor
// make sure tokenBridge transfers are redeemed
// check contributor sale token balances before and after
// check to see if the recipient received refund in fixed-price sale
let saleResult: SealSaleResult;
{
const saleTokenBalancesBefore = await getSaleTokenBalancesOnContributors(
raiseParams.token,
raiseParams.tokenChain
);
const refundRecipientBalanceBefore = await getErc20Balance(
testProvider(CONDUCTOR_NETWORK),
raiseParams.localTokenAddress,
raiseParams.refundRecipient
);
// seal the sale on the conductor contract
saleResult = await sealOrAbortSaleOnEth(saleInit);
expect(saleResult.sale.isSealed, "Sale was not sealed").to.be.true;
// redeem the transfer VAAs on all chains
await redeemCrossChainAllocations(saleResult);
const saleTokenBalancesAfter = await getSaleTokenBalancesOnContributors(
raiseParams.token,
raiseParams.tokenChain
);
// confirm that the right amount of allocations were sent to the contributor contract
for (let i = 0; i < CONTRIBUTOR_NETWORKS.length; i++) {
const chainId = WORMHOLE_ADDRESSES[CONTRIBUTOR_NETWORKS[i]].chainId as ChainId;
const balanceChange = saleTokenBalancesAfter[i].sub(saleTokenBalancesBefore[i]);
const summedAllocation = mockSale.sumAllocationsByChain(mockSaleResults);
expect(
balanceChange.eq(summedAllocation.get(chainId)),
`Incorrect token allocation sent to contributor, balance change: ${balanceChange}, expected change: ${summedAllocation.get(
chainId
)}`
).to.be.true;
}
// sum allocations by chain
const refundRecipientBalanceAfter = await getErc20Balance(
testProvider(CONDUCTOR_NETWORK),
raiseParams.localTokenAddress,
raiseParams.refundRecipient
);
// confirms that the refund recipient received the sale token refund (if applicable)
expect(
refundRecipientBalanceAfter.sub(refundRecipientBalanceBefore).eq(mockSaleResults.tokenRefund),
"Incorrect sale token refund"
).to.be.true;
}
// seal the sale at the contributors
let saleSealedResults;
{
// check the contributor balance before calling saleSealed
const contributorBalancesBefore = await getContributedTokenBalancesOnContributors(acceptedTokens);
// seal the sale on the Contributor contracts
saleSealedResults = await sealSaleAtEthContributors(saleInit, saleResult);
// redeem transfer VAAs for conductor
for (let [chainId, receipt] of saleSealedResults[1]) {
if (chainId != CONDUCTOR_CHAIN_ID) {
await redeemCrossChainContributions(receipt, chainId);
}
}
// check the balance after calling saleSealed
const contributorBalancesAfter = await getContributedTokenBalancesOnContributors(acceptedTokens);
// make sure the balance changes are what we expected
for (let i = 0; i < acceptedTokens.length; i++) {
let expectedBalanceChange = mockSaleResults.allocations[i].totalContribution.sub(
mockSaleResults.allocations[i].excessContribution
);
if ((acceptedTokens[i].tokenChain as ChainId) != CONDUCTOR_CHAIN_ID) {
const nativeAddress = await tryUint8ArrayToNative(
acceptedTokens[i].tokenAddress as Uint8Array,
acceptedTokens[i].tokenChain as ChainId
);
const contributedTokenDecimals = ethers.BigNumber.from(
await getTokenDecimals(acceptedTokens[i].tokenChain as ChainId, nativeAddress)
);
// copy what the token bridge does by norm/denorm based on token decimals
expectedBalanceChange = mockSale.denormalizeAmount(
mockSale.normalizeAmount(expectedBalanceChange, contributedTokenDecimals),
contributedTokenDecimals
);
}
expect(
contributorBalancesBefore[i].sub(contributorBalancesAfter[i]).eq(expectedBalanceChange),
`Incorrect recipient balance change for acceptedToken=${i}`
).to.be.true;
}
}
// claim allocations on contributors
// find unique contributions to claim
const uniqueContributors = findUniqueContributions(contributions, acceptedTokens);
console.log("Claiming contributor allocations and excessContributions if applicable.");
for (let i = 0; i < uniqueContributors.length; i++) {
const successful = await claimContributorAllocationOnEth(saleSealedResults[0], uniqueContributors[i]);
expect(successful, "Failed to claim allocation").to.be.true;
// check to see if there are any excess contributions to claim
if (await excessContributionsExistForSale(saleInit.saleId, uniqueContributors[i])) {
const successful = await claimContributorExcessContributionOnEth(saleSealedResults[0], uniqueContributors[i]);
expect(successful, "Failed to claim excessContribution").to.be.true;
}
}
});
it("Undersubscribed Fixed-price Sale", async () => {
// This test handles undersubscribed sales (totalRaised < minRaise).
// It also updates the sale KYC authority mid-sale,
// and attempts to contribute a "disabled" token.
// increase the minRaise and maxRaise significantly so that the test is unsuccessful
raiseParams["minRaise"] = "999999999";
raiseParams["maxRaise"] = "999999999";
raiseParams["saleDurationSeconds"] += 50; // add some time to test the authority update
// accepted tokens
let acceptedTokens = await buildAcceptedTokens(SALE_CONFIG["acceptedTokens"]);
// test calculator object
const mockSale = new MockSale(
CONDUCTOR_CHAIN_ID,
SALE_CONFIG["denominationDecimals"],
acceptedTokens,
raiseParams,
contributions
);
const mockSaleResults = await mockSale.getResults();
// set up for the disabled tokens test
{
// add new accepted token with an erroneous address (disabled tokens test)
const disabledToken = makeAcceptedToken(CONDUCTOR_CHAIN_ID, CONDUCTOR_ADDRESS, acceptedTokens[0].conversionRate);
acceptedTokens.push(disabledToken);
// now create a fake contribution for the bad token
const fakeContribution: Contribution = {
chainId: CONDUCTOR_CHAIN_ID,
address: CONDUCTOR_ADDRESS,
amount: "420000",
key: contributions[0].key,
};
contributions.unshift(fakeContribution);
}
// create and initialize the sale
const saleInitArray = await createSaleOnEthConductor(
initiatorWallet(CONDUCTOR_NETWORK),
TESTNET_ADDRESSES.conductorAddress,
raiseParams,
acceptedTokens
);
// // initialize the sale on the contributors
const saleInit = await initializeSaleOnEthContributors(saleInitArray[0]);
console.log(saleInit);
console.info("Sale", saleInit.saleId, "has been initialized on the EVM contributors.");
// wait for the sale to start before contributing
console.info("Waiting for the sale to start.");
const extraTime: number = 5; // wait an extra 5 seconds
await waitForSaleToStart(saleInit, extraTime);
// loop through contributors and safe contribute one by one (save one contribution for kyc update test)
console.log("Making contributions to the sale.");
for (const contribution of contributions.slice(0, -1)) {
let successful = false;
// check if we're contributing a solana token
successful = await prepareAndExecuteContribution(saleInit.saleId, raiseParams.token, contribution);
// make sure the disabled token fails
if (contribution.address == CONDUCTOR_ADDRESS) {
expect(successful, "disabled token test failed").to.be.false;
// confirm that the token is disabled
const sale = await getSaleFromContributorOnEth(
TESTNET_ADDRESSES[CHAIN_ID_TO_NETWORK.get(contribution.chainId)],
testProvider(CHAIN_ID_TO_NETWORK.get(contribution.chainId)),
saleInit.saleId
);
expect(sale.disabledAcceptedTokens[acceptedTokens.length - 1], "token was not disabled").to.be.true;
} else {
// make sure real contributions are successful
expect(successful, "Contribution failed").to.be.true;
}
}
// update the authority for the sale and make a contribution
{
// update the sale authority
const authorityUpdatedVaa: Uint8Array = await updateSaleAuthorityOnConductor(saleInit.saleId);
await authorityUpdatedOnEthContributors(authorityUpdatedVaa);
console.log("KYC Authority updated");
// make contribution signed by the new authority
let successful = false;
successful = await prepareAndExecuteContribution(
saleInit.saleId,
raiseParams.token,
contributions[contributions.length - 1],
true
);
expect(successful, "Contribution with new authority failed").to.be.true;
}
// wait for sale to end
console.log("Waiting for the sale to end.");
await waitForSaleToEnd(saleInit, raiseParams.lockUpDurationSeconds); // add the lock up duration
// attest and collect contributions on EVM
const attestVaas: Uint8Array[] = await attestContributionsOnContributor(saleInit);
console.log("Successfully attested contributions on", attestVaas.length, "chains.");
// collect contributions on the conductor
const collectionResults = await collectContributionsOnConductor(attestVaas, saleInit.saleId);
for (const result of collectionResults) {
expect(result, "Failed to collect all contributions on the conductor.").to.be.true;
}
console.log("Successfully collected contributions on the conductor.");
// seal the sale on the conductor
// make sure the refundRecipient has received the refund
// abort the sale on the contributor contracts
let saleResult: SealSaleResult;
{
const refundRecipientBalanceBefore = await getErc20Balance(
testProvider(CONDUCTOR_NETWORK),
raiseParams.localTokenAddress,
raiseParams.refundRecipient
);
// seal or abort the sale on the conductor contract
saleResult = await sealOrAbortSaleOnEth(saleInit);
expect(saleResult.sale.isAborted, "Sale was not aborted").to.be.true;
// sum allocations by chain
const refundRecipientBalanceAfter = await getErc20Balance(
testProvider(CONDUCTOR_NETWORK),
raiseParams.localTokenAddress,
raiseParams.refundRecipient
);
// abort the sale on the contributor contracts
await abortSaleAtContributors(saleResult);
console.log("Successfully aborted the sale on the contributors.");
// confirms that the refund recipient received the sale token refund (if applicable)
expect(
refundRecipientBalanceAfter.sub(refundRecipientBalanceBefore).eq(mockSaleResults.tokenRefund),
"Incorrect sale token refund"
).to.be.true;
}
// claim allocations on contributors
// find unique refunds to claim
const uniqueContributors = findUniqueContributions(contributions, acceptedTokens);
console.log("Claiming contributor refunds.");
for (let i = 0; i < uniqueContributors.length; i++) {
// skip the disabled token (which uses the Conductor's address)
if (uniqueContributors[i].address != CONDUCTOR_ADDRESS) {
const successful = await claimRefundForContributorOnEth(saleInit, uniqueContributors[i]);
expect(successful, "Failed to claim refund").to.be.true;
}
}
});
});

File diff suppressed because it is too large Load Diff

18
sdk/src/testnet/solana.ts Normal file
View File

@ -0,0 +1,18 @@
import { AnchorProvider, web3, Program } from "@project-serum/anchor";
import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
import { AnchorContributor } from "../../target/types/anchor_contributor";
import AnchorContributorIdl from "../../target/idl/anchor_contributor.json";
export function connectToContributorProgram(
connection: web3.Connection,
wallet: web3.Keypair,
programId: web3.PublicKey,
options: web3.ConfirmOptions = {}
): Program<AnchorContributor> {
const program = new Program<AnchorContributor>(
AnchorContributorIdl as any,
programId,
new AnchorProvider(connection, new NodeWallet(wallet), options)
);
return program;
}

View File

@ -1,8 +1,9 @@
import { ConductorSale } from "wormhole-icco-sdk";
import { ConductorSale } from "../../src";
import { ChainId } from "@certusone/wormhole-sdk";
import { ethers } from "ethers";
export interface saleParams {
export interface SaleParams {
isFixedPrice: boolean;
token: string;
localTokenAddress: string;
tokenAmount: string;
@ -13,8 +14,9 @@ export interface saleParams {
recipient: string;
refundRecipient: string;
saleDurationSeconds: number;
lockUpDurationSeconds: number;
saleStartTimer: number;
solanaTokenAccount: string;
authority: string;
}
export interface TokenConfig {
@ -66,15 +68,12 @@ export interface SaleInit {
tokenAddress: string;
tokenChain: number;
tokenDecimals: number;
tokenAmount: ethers.BigNumberish;
minRaise: ethers.BigNumberish;
maxRaise: ethers.BigNumberish;
saleStart: ethers.BigNumberish;
saleEnd: ethers.BigNumberish;
acceptedTokens: AcceptedToken[];
solanaTokenAccount: ethers.BytesLike;
recipient: string;
refundRecipient: string;
authority: string;
unlockTimestamp: ethers.BigNumberish;
}
export interface SolanaToken {

View File

@ -0,0 +1,184 @@
import { ethers } from "ethers";
import { AcceptedToken } from "../icco";
import { ChainId, tryUint8ArrayToNative, CHAIN_ID_SOLANA } from "@certusone/wormhole-sdk";
import { SaleParams, Contribution } from "./structs";
import { getTokenDecimals } from "./utils";
interface Allocations {
tokenIndex: number;
allocation: ethers.BigNumber;
excessContribution: ethers.BigNumber;
totalContribution: ethers.BigNumber;
}
interface Results {
tokenRefund: ethers.BigNumber;
allocations: Allocations[];
}
export class MockSale {
conductorChainId: ChainId;
saleTokenDecimals: number;
acceptedTokens: AcceptedToken[];
raiseParams: SaleParams;
contributions: Contribution[];
constructor(
conductorChainId: ChainId,
denominationDecimals: number,
acceptedTokens: AcceptedToken[],
raiseParams: SaleParams,
contributions: Contribution[]
) {
this.conductorChainId = conductorChainId;
this.saleTokenDecimals = denominationDecimals;
this.acceptedTokens = acceptedTokens;
this.raiseParams = raiseParams;
this.contributions = contributions;
}
sumAllocationsByChain(results: Results): Map<ChainId, ethers.BigNumber> {
const summedAllocations: Map<ChainId, ethers.BigNumber> = new Map<ChainId, ethers.BigNumber>();
for (let i = 0; i < this.acceptedTokens.length; i++) {
const chainId = this.acceptedTokens[i].tokenChain as ChainId;
if (!summedAllocations.has(chainId)) {
summedAllocations.set(chainId, results.allocations[i].allocation);
} else {
let currentAllocation = summedAllocations.get(chainId);
summedAllocations.set(chainId, currentAllocation.add(results.allocations[i].allocation));
}
}
return summedAllocations;
}
getTokenIndexFromConfig(chainId: ChainId, address: string): number {
for (let i = 0; i < this.acceptedTokens.length; i++) {
let nativeTokenAddress = tryUint8ArrayToNative(
this.acceptedTokens[i].tokenAddress as Uint8Array,
this.acceptedTokens[i].tokenChain as ChainId
);
if (this.acceptedTokens[i].tokenChain !== CHAIN_ID_SOLANA) {
nativeTokenAddress = ethers.utils.getAddress(nativeTokenAddress);
}
if (chainId === (this.acceptedTokens[i].tokenChain as ChainId) && address === nativeTokenAddress) {
return i;
}
}
}
normalizeAmount(amount: ethers.BigNumber, decimals: ethers.BigNumber): ethers.BigNumber {
let maxDecimals = ethers.BigNumber.from("8");
if (decimals.gt(maxDecimals)) {
return amount.div(ethers.BigNumber.from("10").pow(decimals.sub(maxDecimals)));
} else {
return amount;
}
}
denormalizeAmount(amount: ethers.BigNumber, decimals: ethers.BigNumber): ethers.BigNumber {
let maxDecimals = ethers.BigNumber.from("8");
if (decimals.gt(maxDecimals)) {
return amount.mul(ethers.BigNumber.from("10").pow(decimals.sub(maxDecimals)));
} else {
return amount;
}
}
async sumContributions(): Promise<ethers.BigNumber[]> {
// create array of size acceptedTokens.length of zeros
const rawTotals = new Array<ethers.BigNumber>(this.acceptedTokens.length).fill(ethers.BigNumber.from(0));
// create map
for (const contribution of this.contributions) {
const tokenIndex = this.getTokenIndexFromConfig(contribution.chainId, contribution.address);
const tokenDecimals = await getTokenDecimals(contribution.chainId as ChainId, contribution.address);
const contributionAmount = ethers.utils.parseUnits(contribution.amount, tokenDecimals);
// add the contribution to the running total
rawTotals[tokenIndex] = rawTotals[tokenIndex].add(contributionAmount);
}
return rawTotals;
}
calculateTotalRaised(totalContributions: ethers.BigNumber[]): ethers.BigNumber {
let totalRaised = ethers.BigNumber.from(0);
for (let i = 0; i < totalContributions.length; i++) {
const scaledContribution = totalContributions[i]
.mul(this.acceptedTokens[i].conversionRate)
.div(ethers.utils.parseEther("1"));
totalRaised = totalRaised.add(scaledContribution);
}
return totalRaised;
}
async calculateResults(totalContributions: ethers.BigNumber[]): Promise<Results> {
const minRaise = ethers.utils.parseUnits(this.raiseParams.minRaise, this.saleTokenDecimals);
const maxRaise = ethers.utils.parseUnits(this.raiseParams.maxRaise, this.saleTokenDecimals);
let saleTokenAmount = ethers.utils.parseUnits(this.raiseParams.tokenAmount, this.saleTokenDecimals);
let totalRaised = this.calculateTotalRaised(totalContributions);
let totalAllocated = ethers.BigNumber.from(0);
// calculate the token refund to send to reFundRecipient
// and the total excess contribution (if applicable)
let tokenRefund = ethers.BigNumber.from(0);
let totalExcessContribution = ethers.BigNumber.from(0);
if (!totalRaised.gte(maxRaise)) {
tokenRefund = saleTokenAmount.sub(saleTokenAmount.mul(totalRaised).div(maxRaise));
saleTokenAmount = saleTokenAmount.sub(tokenRefund);
} else {
totalExcessContribution = totalRaised.sub(maxRaise);
}
// allocations container
const allocations: Allocations[] = [];
// compute the allocations
for (let i = 0; i < totalContributions.length; i++) {
const scaledContribution = totalContributions[i]
.mul(this.acceptedTokens[i].conversionRate)
.div(ethers.utils.parseEther("1"));
let allocation = ethers.BigNumber.from(0);
let excessContribution = ethers.BigNumber.from(0);
if (totalRaised.gte(minRaise)) {
allocation = saleTokenAmount.mul(scaledContribution).div(totalRaised);
excessContribution = totalExcessContribution.mul(totalContributions[i]).div(totalRaised);
if ((this.acceptedTokens[i].tokenChain as ChainId) !== this.conductorChainId) {
// normalize the bridge transfers
allocation = this.denormalizeAmount(
this.normalizeAmount(allocation, ethers.BigNumber.from(this.saleTokenDecimals)),
ethers.BigNumber.from(this.saleTokenDecimals)
);
}
// create the allocation
allocations.push({
tokenIndex: i,
allocation: allocation,
excessContribution: excessContribution,
totalContribution: totalContributions[i],
});
}
// keep running total of allocations
totalAllocated = totalAllocated.add(allocation);
}
// store the results and return
const results: Results = {
tokenRefund: tokenRefund.add(saleTokenAmount.sub(totalAllocated)),
allocations: allocations,
};
return results;
}
async getResults(): Promise<Results> {
const totalContributions: ethers.BigNumber[] = await this.sumContributions();
return await this.calculateResults(totalContributions);
}
}

1063
sdk/src/testnet/utils.ts Normal file

File diff suppressed because it is too large Load Diff

41
sdk/src/tools/README.md Normal file
View File

@ -0,0 +1,41 @@
# Details
This directory contains scripts for contract registration, and proxy pattern upgrades.
### Dependencies
Both `register_testnet_contributors.ts` and `upgrade_testnet_contracts.ts` depend on the following files:
- `testnet.json` - contains the deployed ICCO contract addresses in testnet
- `ethereum/icco_deployment_config.js` - contains rpc providers and wallet private keys
### Building
Run the following command in the root directory to build the ICCO tools:
```sh
make sdk
```
# Example Contract Registration
```sh
ts-node register_testnet_contributors.ts --network goerli fuji binance_testnet
```
### Arguments
- **--network** is required. The list of `Contributor` contract networks that will be registered
# Example Contract Upgrade
Before upgrading the contract, the implementation address must exist in `testnet.json`
```sh
ts-node upgrade_testnet_contracts.ts --contractType contributor --network goerli
```
### Arguments
- **--network** is required. The `Contributor` or `Conductor` contract network that will be upgraded.
- **--contractType** is required. The type of contract to upgrade.

View File

@ -1,12 +1,12 @@
import yargs from "yargs";
import { registerChainOnEth, nativeToUint8Array } from "wormhole-icco-sdk";
import { tryNativeToUint8Array } from "@certusone/wormhole-sdk";
import { registerChainOnEth } from "../icco/registerChain";
import { tryNativeToUint8Array, tryUint8ArrayToNative } from "@certusone/wormhole-sdk";
import { ethers } from "ethers";
import { web3 } from "@project-serum/anchor";
import { findProgramAddressSync } from "@project-serum/anchor/dist/cjs/utils/pubkey";
const fs = require("fs");
const DeploymentConfig = require("../../ethereum/icco_deployment_config.js");
import * as DeploymentConfig from "../../../ethereum/icco_deployment_config.js";
const ConductorConfig = DeploymentConfig["conductor"];
function parseArgs(): string[] {
@ -19,6 +19,7 @@ function parseArgs(): string[] {
.help("h")
.alias("h", "help").argv;
// @ts-ignore
const args: string[] = parsed.network;
return args;
}
@ -26,20 +27,13 @@ function parseArgs(): string[] {
async function main() {
const networks = parseArgs();
for (let i = 0; i < networks.length; i++) {
let config;
if (networks[i] == "solana_emitter") {
// we're registering the solana contributor emitter address
// but this doesn't have a key in the Deployment config
config = DeploymentConfig["solana_testnet"];
} else {
config = DeploymentConfig[networks[i]];
}
for (const network of networks) {
const config = DeploymentConfig[network];
if (!config) {
throw Error("deployment config undefined");
}
const testnet = JSON.parse(fs.readFileSync(`${__dirname}/../../testnet.json`, "utf8"));
const testnet = JSON.parse(fs.readFileSync(`${__dirname}/../../../testnet.json`, "utf8"));
// create wallet to call sdk method with
const provider = new ethers.providers.JsonRpcProvider(ConductorConfig.rpc);
@ -48,14 +42,13 @@ async function main() {
// if it's a solana registration - create 32 byte address
let contributorAddressBytes: Uint8Array;
if (config.contributorChainId == 1) {
contributorAddressBytes = tryNativeToUint8Array(testnet[networks[i]], "solana");
const solanaProgId = new web3.PublicKey(contributorAddressBytes);
const [key, bump] = findProgramAddressSync([Buffer.from("emitter")], solanaProgId);
contributorAddressBytes = key.toBuffer();
console.log("solana contributorEmitter address: ", key.toBase58());
contributorAddressBytes = tryNativeToUint8Array(testnet[network], "solana");
const programId = new web3.PublicKey(contributorAddressBytes);
const [key, _] = findProgramAddressSync([Buffer.from("emitter")], programId);
contributorAddressBytes = tryNativeToUint8Array(key.toString(), "solana");
} else {
// convert contributor address to bytes
contributorAddressBytes = nativeToUint8Array(testnet[networks[i]], config.contributorChainId);
contributorAddressBytes = tryNativeToUint8Array(testnet[network], config.contributorChainId);
}
try {
@ -69,11 +62,11 @@ async function main() {
);
// output hash
console.info("Registering contributor on network:", networks[i], "txHash:", tx.transactionHash);
console.info("Registering contributor on network:", network, "txHash:", tx.transactionHash);
} catch (error: any) {
const errorMsg = error.toString();
if (errorMsg.includes("chain already registered")) {
console.info(networks[i], "has already been registered!");
if (errorMsg.includes("2")) {
console.info(network, "has already been registered!");
} else {
console.log(errorMsg);
}

View File

@ -1,9 +1,9 @@
import yargs from "yargs";
import { ethers } from "ethers";
import { Conductor__factory, Contributor__factory } from "wormhole-icco-sdk";
import { Conductor__factory, Contributor__factory } from "../ethers-contracts";
const fs = require("fs");
const DeploymentConfig = require("../../ethereum/icco_deployment_config.js");
import * as DeploymentConfig from "../../../ethereum/icco_deployment_config.js";
function parseArgs(): string[] {
const parsed = yargs(process.argv.slice(2))
@ -12,15 +12,16 @@ function parseArgs(): string[] {
description: "Type of contract (e.g. conductor)",
require: true,
})
.options("network", {
.option("network", {
type: "string",
description: "Network to deploy to (e.g. goerli)",
require: true,
required: true,
})
.help("h")
.alias("h", "help").argv;
const args = [parsed.contractType, parsed.network];
// @ts-ignore
const args: string[] = [parsed.contractType, parsed.network];
return args;
}
@ -36,9 +37,7 @@ async function main() {
throw Error("deployment config undefined");
}
const testnet = JSON.parse(
fs.readFileSync(`${__dirname}/../../testnet.json`, "utf8")
);
const testnet = JSON.parse(fs.readFileSync(`${__dirname}/../../../testnet.json`, "utf8"));
// create wallet to call sdk method with
const provider = new ethers.providers.JsonRpcProvider(config.rpc);
@ -50,10 +49,7 @@ async function main() {
let newImplementation;
if (contractType == "conductor") {
contractFactory = Conductor__factory.connect(
testnet["conductorAddress"],
wallet
);
contractFactory = Conductor__factory.connect(testnet["conductorAddress"], wallet);
chainId = config.conductorChainId;
newImplementation = testnet[network.concat("ConductorImplementation")];
} else {
@ -66,12 +62,7 @@ async function main() {
const tx = await contractFactory.upgrade(chainId, newImplementation);
const receipt = await tx.wait();
console.log(
"transction:",
receipt.transactionHash,
", newImplementation:",
newImplementation
);
console.log("transction:", receipt.transactionHash, ", newImplementation:", newImplementation);
return;
}

131
sdk/src/utils.ts Normal file
View File

@ -0,0 +1,131 @@
import {
ChainId,
CHAIN_ID_SOLANA,
ERC20__factory,
getForeignAssetEth,
tryNativeToUint8Array,
tryUint8ArrayToNative,
uint8ArrayToHex,
} from "@certusone/wormhole-sdk";
import { getForeignAssetSolana } from "@certusone/wormhole-sdk";
import { BN, web3 } from "@project-serum/anchor";
import { ethers } from "ethers";
import { AcceptedToken, Raise } from "./icco";
import { getCurrentTime } from "./testnet/utils";
export class SaleParameters {
// conductor
conductorChain: ChainId;
// for accepted token conversion rate calculations
precision: number;
denominationDecimals: number;
// parameters
raise: Raise;
acceptedTokens: AcceptedToken[];
tokenChain: ChainId;
tokenAddress: string;
constructor(conductorChain: ChainId, denominationDecimals: number) {
this.conductorChain = conductorChain;
this.denominationDecimals = denominationDecimals;
this.acceptedTokens = [];
this.precision = 18;
}
setSaleToken(chain: ChainId, address: string) {
this.tokenChain = chain;
this.tokenAddress = address;
}
prepareRaise(
isFixedPrice: boolean,
tokenAmount: string,
recipient: string,
refundRecipient: string,
minRaise: string,
maxRaise: string,
custodianSaleTokenAccount: web3.PublicKey,
authority: string
) {
// TODO: handle raise stuff
this.raise = {
isFixedPrice,
token: tryNativeToUint8Array(this.tokenAddress, this.tokenChain),
tokenAmount: tokenAmount,
tokenChain: this.tokenChain,
minRaise,
maxRaise,
saleStart: 0, // placeholder
saleEnd: 0, // placeholder
unlockTimestamp: 0, // placeholder
recipient,
refundRecipient,
solanaTokenAccount: tryNativeToUint8Array(custodianSaleTokenAccount.toString(), CHAIN_ID_SOLANA),
authority,
};
}
saleTokenAsArray() {
return tryNativeToUint8Array(this.tokenAddress, this.tokenChain);
}
async saleTokenSolanaMint(connection?: web3.Connection, solanaTokenBridge?: string) {
if (this.tokenChain == CHAIN_ID_SOLANA) {
return new web3.PublicKey(this.tokenAddress);
}
const wrapped = await getForeignAssetSolana(
connection,
solanaTokenBridge,
this.tokenChain,
this.saleTokenAsArray()
);
return new web3.PublicKey(wrapped);
}
async saleTokenEvm(connection: ethers.providers.Provider, tokenBridge: string) {
const wrapped = await getForeignAssetEth(tokenBridge, connection, this.tokenChain, this.saleTokenAsArray());
return ERC20__factory.connect(wrapped, connection);
}
makeRaiseNow(startDelay: number, saleDuration: number, unlockPeriod: number) {
this.raise.saleStart = getCurrentTime() + startDelay;
this.raise.saleEnd = this.raise.saleStart + saleDuration;
this.raise.unlockTimestamp = this.raise.saleEnd + unlockPeriod;
return this.raise;
}
addAcceptedToken(chain: ChainId, address: string, priceConversion: string, nativeDecimals: number) {
// we need to ensure that amounts will add up to the same units as the denomination
// of the raise
const normalization = this.denominationDecimals + this.precision - nativeDecimals;
this.acceptedTokens.push({
tokenAddress: tryNativeToUint8Array(address, chain),
tokenChain: chain as number,
conversionRate: ethers.utils.parseUnits(priceConversion, normalization).toString(),
});
}
}
export function parseIccoHeader(iccoSignedVaa: Buffer): [number, Buffer] {
const numSigners = iccoSignedVaa[5];
const payloadStart = 57 + 66 * numSigners;
return [iccoSignedVaa[payloadStart], iccoSignedVaa.subarray(payloadStart + 1, payloadStart + 33)];
}
export function bytesLikeToHex(byteslike: ethers.BytesLike) {
return uint8ArrayToHex(ethers.utils.arrayify(byteslike));
}
export function unitsToUintString(value: string, decimals: number) {
return ethers.utils.parseUnits(value, decimals).toString();
}
export function unitsToUint(value: string, decimals: number) {
return new BN(unitsToUintString(value, decimals));
}

18
sdk/tsconfig.json Normal file
View File

@ -0,0 +1,18 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"typeRoots": ["./node_modules/@types"],
"module": "commonjs",
"target": "es2020",
"moduleResolution": "node",
"declaration": true,
"outDir": "./lib/esm",
"strict": false,
"esModuleInterop": true,
"downlevelIteration": true,
"resolveJsonModule": true,
"lib": ["es2020.bigint"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

Some files were not shown because too many files have changed in this diff Show More