Skip to content

Commit

Permalink
Add Algorand platform (#160)
Browse files Browse the repository at this point in the history
  • Loading branch information
barnjamin authored Dec 29, 2023
1 parent 9e7435c commit 1a9d8fb
Show file tree
Hide file tree
Showing 44 changed files with 3,450 additions and 604 deletions.
6 changes: 2 additions & 4 deletions connect/src/protocols/tokenTransfer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,6 @@ export class TokenTransfer<N extends Network = Network>
const fromChain = this.wh.getChain(this.transfer.from.chain);
this.txids = await TokenTransfer.transfer<N>(fromChain, this.transfer, signer);
this._state = TransferState.SourceInitiated;

return this.txids.map(({ txid }) => txid);
}

Expand Down Expand Up @@ -225,7 +224,6 @@ export class TokenTransfer<N extends Network = Network>
timeout,
);
}

this._state = TransferState.Attested;
return this.attestations.map((vaa) => vaa.id);
}
Expand Down Expand Up @@ -254,8 +252,8 @@ export class TokenTransfer<N extends Network = Network>
attestation as TokenTransferVAA,
signer,
);

this.txids.push(...redeemTxids);
this._state = TransferState.DestinationInitiated;
return redeemTxids.map(({ txid }) => txid);
}

Expand Down Expand Up @@ -369,7 +367,7 @@ export class TokenTransfer<N extends Network = Network>
const tb = await srcChain.getTokenBridge();
// otherwise, check to see if it is a wrapped token locally
lookup = await tb.getOriginalAsset(token.address);
} catch {
} catch (e) {
// not a from-chain native wormhole-wrapped one
lookup = token;
}
Expand Down
4 changes: 3 additions & 1 deletion core/base/src/constants/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const rpcConfig = [[
["Wormchain", "https://wormchain-rpc.quickapi.com"],
["Xpla", "https://dimension-rpc.xpla.dev"],
["Sei", "https://sei-rpc.polkachu.com/"],
["Algorand", "https://mainnet-api.algonode.cloud"],
]], [
"Testnet", [
["Ethereum", "https://rpc.ankr.com/eth_goerli"],
Expand All @@ -44,7 +45,8 @@ const rpcConfig = [[
["Evmos", "https://evmos-testnet-rpc.polkachu.com"],
["Wormchain", "https://gateway.testnet.xlabs.xyz/"],
["Xpla", "https://cube-rpc.xpla.dev"],
["Sepolia", "https://ethereum-sepolia.publicnode.com"]
["Sepolia", "https://ethereum-sepolia.publicnode.com"],
["Algorand", "https://testnet-api.algonode.cloud"],
]], [
"Devnet", [
["Ethereum", "http://eth-devnet:8545"],
Expand Down
31 changes: 22 additions & 9 deletions core/base/src/utils/encoding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,34 @@ export const b58 = {
};

export const bignum = {
decode: (input: string | Uint8Array) =>
typeof input === "string" ? BigInt(input) : BigInt(hex.encode(input, true)),
encode: (input: bigint, prefix: boolean = false) => (prefix ? "0x" : "") + input.toString(16),
decode: (input: string | Uint8Array) => {
if (typeof input !== "string") input = hex.encode(input, true);
if (input === "" || input === "0x") return 0n;
return BigInt(input);
},
encode: (input: bigint, prefix: boolean = false) => bignum.toString(input, prefix),
toString: (input: bigint, prefix: boolean = false) => {
let str = input.toString(16);
str = str.length % 2 === 1 ? (str = "0" + str) : str;
if (prefix) return "0x" + str;
return str;
},
toBytes: (input: bigint | number, length?: number) => {
const b = hex.decode(bignum.toString(typeof input === "number" ? BigInt(input) : input));
if (!length) return b;
return bytes.zpad(b, length);
},
};

export const bytes = {
encode: (value: string | bigint): Uint8Array =>
typeof value === "bigint"
? bytes.encode(bignum.encode(value))
: new TextEncoder().encode(value),
encode: (value: string): Uint8Array => new TextEncoder().encode(value),
decode: (value: Uint8Array): string => new TextDecoder().decode(value),
equals: (lhs: Uint8Array, rhs: Uint8Array): boolean =>
lhs.length === rhs.length && lhs.every((v, i) => v === rhs[i]),
zpad: (arr: Uint8Array, length: number): Uint8Array =>
bytes.concat(new Uint8Array(length - arr.length), arr),
zpad: (arr: Uint8Array, length: number, padStart: boolean = true): Uint8Array =>
padStart
? bytes.concat(new Uint8Array(length - arr.length), arr)
: bytes.concat(arr, new Uint8Array(length - arr.length)),
concat: (...args: Uint8Array[]): Uint8Array => {
const length = args.reduce((acc, curr) => acc + curr.length, 0);
const result = new Uint8Array(length);
Expand Down
9 changes: 6 additions & 3 deletions core/definitions/src/universalAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const algorandAppIdLayout = [

export class UniversalAddress implements Address {
static readonly byteSize = 32;
readonly type: string = "Universal";
static readonly type: string = "Universal";

private readonly address: Uint8Array;

Expand Down Expand Up @@ -50,9 +50,12 @@ export class UniversalAddress implements Address {
return !throws(() => UniversalAddress.stringToUint8Array(address, format));
}

//TODO isn't this quite the code smell? - why would we have to test an any?
static instanceof(address: any): address is UniversalAddress {
return typeof address === "object" && "type" in address && address.type === "Universal";
return (
typeof address === "object" &&
"constructor" in address &&
address.constructor.type === "Universal"
);
}

private static stringToUint8Array(address: string, format: PlatformAddressFormat): Uint8Array {
Expand Down
7 changes: 5 additions & 2 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
},
"sideEffects": false,
"scripts": {
"wrapped": "tsx src/createWrapped.ts",
"tb": "tsx src/tokenBridge.ts",
"cctp": "tsx src/cctp.ts",
"demo": "tsx src/index.ts",
Expand All @@ -46,7 +47,8 @@
"docs": "typedoc"
},
"devDependencies": {
"dotenv": "^16.3.1"
"dotenv": "^16.3.1",
"tsx": "^4.7.0"
},
"dependencies": {
"@wormhole-foundation/connect-sdk": "^0.3.0-beta.6",
Expand All @@ -61,6 +63,7 @@
"@wormhole-foundation/connect-sdk-cosmwasm-tokenbridge": "^0.3.0-beta.6",
"@wormhole-foundation/connect-sdk-evm-cctp": "^0.3.0-beta.6",
"@wormhole-foundation/connect-sdk-solana-cctp": "^0.3.0-beta.6",
"@wormhole-foundation/connect-sdk-cosmwasm-ibc": "^0.3.0-beta.6"
"@wormhole-foundation/connect-sdk-cosmwasm-ibc": "^0.3.0-beta.6",
"algosdk": "^2.7.0"
}
}
91 changes: 91 additions & 0 deletions examples/src/createWrapped.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { TokenId, Wormhole, signSendWait } from "@wormhole-foundation/connect-sdk";
import { AlgorandPlatform } from "@wormhole-foundation/connect-sdk-algorand";
import { SolanaPlatform } from "@wormhole-foundation/connect-sdk-solana";
import { getStuff } from "./helpers";

import "@wormhole-foundation/connect-sdk-algorand-tokenbridge";
import "@wormhole-foundation/connect-sdk-solana-tokenbridge";

(async function () {
const wh = new Wormhole("Testnet", [AlgorandPlatform, SolanaPlatform]);

// Original Token to Attest
const token: TokenId = Wormhole.chainAddress("Algorand", "10458941");

// grab context and signer
const origChain = wh.getChain(token.chain);
const { signer: origSigner } = await getStuff(origChain);

// Note: if the VAA is not produced before the attempt to retrieve it times out
// you should set this value to the txid logged in the previous run
let txid = undefined;
// txid = "0x55127b9c8af46aaeea9ef28d8bf91e1aff920422fc1c9831285eb0f39ddca2fe";

txid = "FPNHIFFUZDVPT5SATZQZZ7DFGZMPCCHEFBCB5EZQJV4RRK3ZYTVA";
txid = "GWZU432ERFU3NES4MA7IAAP6DX73F5VRSSIWGJVC5JRHOH6UMWEQ";

if (!txid) {
// create attestation from origin chain, the same VAA
// can be used across all chains
const tb = await origChain.getTokenBridge();
const attestTxns = tb.createAttestation(
token.address,
Wormhole.parseAddress(origSigner.chain(), origSigner.address()),
);
const txids = await signSendWait(origChain, attestTxns, origSigner);
txid = txids[0].txid;
console.log("Created attestation (save this): ", txid);
}

// Get the wormhole message id from the transaction logs
const msgs = await origChain.parseTransaction(txid);
console.log(msgs);

// Get the Signed VAA from the API
const timeout = 60_000; // 60 seconds
const vaa = await wh.getVaa(msgs[0], "TokenBridge:AttestMeta", timeout);
if (!vaa) throw new Error("VAA not found after retries exhausted, try extending the timeout");

console.log(vaa.payload.token.address);

// Check if its attested and if not
// submit the attestation to the token bridge on the
// destination chain
const chain = "Solana";
const destChain = wh.getChain(chain);
const { signer } = await getStuff(destChain);

// grab a ref to the token bridge
const tb = await destChain.getTokenBridge();
try {
// try to get the wrapped version, an error here likely means
// its not been attested
const wrapped = await tb.getWrappedAsset(token);
console.log("already wrapped");
return { chain, address: wrapped };
} catch (e) {}

// no wrapped asset, needs to be attested
console.log("attesting asset");
await signSendWait(
destChain,
tb.submitAttestation(vaa, Wormhole.parseAddress(signer.chain(), signer.address())),
signer,
);

async function waitForIt() {
do {
// check again
try {
const wrapped = await tb.getWrappedAsset(token);
return { chain, address: wrapped };
} catch (e) {
console.error(e);
}
console.log("Waiting before checking again...");
await new Promise((r) => setTimeout(r, 2000));
} while (true);
}

console.log("wrapped: ", await waitForIt());
})();
6 changes: 5 additions & 1 deletion examples/src/helpers/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import {
} from "@wormhole-foundation/connect-sdk";

// Importing from src so we dont have to rebuild to see debug stuff in signer
import { getCosmwasmSigner } from "@wormhole-foundation/connect-sdk-cosmwasm/src/testing";
import { getEvmSigner } from "@wormhole-foundation/connect-sdk-evm/src/testing";
import { getSolanaSigner } from "@wormhole-foundation/connect-sdk-solana/src/testing";
import { getCosmwasmSigner } from "@wormhole-foundation/connect-sdk-cosmwasm/src/testing";
import { getAlgorandSigner } from "@wormhole-foundation/connect-sdk-algorand/src/testing";

// read in from `.env`
require("dotenv").config();
Expand Down Expand Up @@ -60,6 +61,9 @@ export async function getStuff<
case "Evm":
signer = await getEvmSigner(await chain.getRpc(), getEnv("ETH_PRIVATE_KEY"));
break;
case "Algorand":
signer = await getAlgorandSigner(await chain.getRpc(), getEnv("ALGORAND_MNEMONIC"));
break;
default:
throw new Error("Unrecognized platform: " + platform);
}
Expand Down
21 changes: 13 additions & 8 deletions examples/src/messaging.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import { Wormhole, encoding, signSendWait } from "@wormhole-foundation/connect-sdk";
import { SolanaPlatform } from "@wormhole-foundation/connect-sdk-solana";
import { AlgorandPlatform } from "@wormhole-foundation/connect-sdk-algorand";
import { getStuff } from "./helpers";

// register the protocol
import "@wormhole-foundation/connect-sdk-solana-core";
import "@wormhole-foundation/connect-sdk-algorand-core";

(async function () {
const wh = new Wormhole("Testnet", [SolanaPlatform]);
const wh = new Wormhole("Testnet", [AlgorandPlatform]);

const chain = wh.getChain("Solana");
const chain = wh.getChain("Algorand");
const { signer, address } = await getStuff(chain);

// Get a reference to the core messaging bridge
const coreBridge = await chain.getWormholeCore();

// Generate transactions, sign and send them
const publishTxs = coreBridge.publishMessage(address.address, encoding.bytes.encode("lol"), 0, 0);
const [txid] = await signSendWait(chain, publishTxs, signer);
const txids = await signSendWait(chain, publishTxs, signer);

// Grab the wormhole message from the transaction logs or storage
// Take the last txid in case multiple were sent
// the last one should be the one containing the relevant
// event or log info
const txid = txids[txids.length - 1];

// // Grab the wormhole message from the transaction logs or storage
const [whm] = await chain.parseTransaction(txid!.txid);

// Wait for the vaa with a timeout,
// // Wait for the vaa with a timeout,
const vaa = await wh.getVaa(whm!, "Uint8Array", 60_000);
console.log(vaa);
// can also search by txid but it takes longer to show up
// console.log(await wh.getVaaByTxHash(txid!.txid, "Uint8Array"));

const verifyTxs = coreBridge.verifyMessage(address.address, vaa!);
await signSendWait(chain, verifyTxs, signer);
console.log(await signSendWait(chain, verifyTxs, signer));

// reposting the vaa should result in 0 transactions being issued
// assuming you're reading your writes
Expand Down
12 changes: 8 additions & 4 deletions examples/src/tokenBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,26 @@ import { TransferStuff, getStuff, waitLog } from "./helpers";
// Import the platform specific packages
import { EvmPlatform } from "@wormhole-foundation/connect-sdk-evm";
import { SolanaPlatform } from "@wormhole-foundation/connect-sdk-solana";
import { AlgorandPlatform } from "@wormhole-foundation/connect-sdk-algorand";

// Register the protocols
import "@wormhole-foundation/connect-sdk-evm-tokenbridge";
import "@wormhole-foundation/connect-sdk-solana-tokenbridge";
import "@wormhole-foundation/connect-sdk-algorand-tokenbridge";

(async function () {
// init Wormhole object, passing config for which network
// to use (e.g. Mainnet/Testnet) and what Platforms to support
const wh = new Wormhole("Testnet", [EvmPlatform, SolanaPlatform]);
const wh = new Wormhole("Testnet", [EvmPlatform, SolanaPlatform, AlgorandPlatform]);

// Grab chain Contexts -- these hold a reference to a cached rpc client
const sendChain = wh.getChain("Avalanche");
const sendChain = wh.getChain("Algorand");
const rcvChain = wh.getChain("Solana");

// shortcut to allow transferring native gas token
const token: TokenId<"Avalanche"> | "native" = "native";
const token: TokenId | "native" = "native";

// const token = Wormhole.chainAddress("Algorand", "10458941"); // USDC on Algorand

// A TokenId is just a `{chain, address}` pair and an alias for ChainAddress
// The `address` field must be a parsed address.
Expand All @@ -41,7 +45,7 @@ import "@wormhole-foundation/connect-sdk-solana-tokenbridge";
// Note: The Token bridge will dedust past 8 decimals
// this means any amount specified past that point will be returned
// to the caller
const amount = "0.15";
const amount = "0.001";

// With automatic set to true, perform an automatic transfer. This will invoke a relayer
// contract intermediary that knows to pick up the transfers
Expand Down
Loading

0 comments on commit 1a9d8fb

Please sign in to comment.