فهرست منبع

add nft bridge sdk

Change-Id: Ib91f8ca6c078eb2c2145550ffed4c6c3b7186573
Hendrik Hofstadt 4 سال پیش
والد
کامیت
b77751788b

+ 70 - 0
sdk/js/src/nft_bridge/getForeignAsset.ts

@@ -0,0 +1,70 @@
+import { Connection, PublicKey } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { Bridge__factory } from "../ethers-contracts";
+import { ChainId } from "../utils";
+import { LCDClient } from "@terra-money/terra.js";
+import { fromUint8Array } from "js-base64";
+
+/**
+ * Returns a foreign asset address on Ethereum for a provided native chain and asset address, AddressZero if it does not exist
+ * @param tokenBridgeAddress
+ * @param provider
+ * @param originChain
+ * @param originAsset zero pad to 32 bytes
+ * @returns
+ */
+export async function getForeignAssetEth(
+  tokenBridgeAddress: string,
+  provider: ethers.providers.Web3Provider,
+  originChain: ChainId,
+  originAsset: Uint8Array
+) {
+  const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
+  try {
+    return await tokenBridge.wrappedAsset(originChain, originAsset);
+  } catch (e) {
+    return ethers.constants.AddressZero;
+  }
+}
+
+export async function getForeignAssetTerra(
+  tokenBridgeAddress: string,
+  client: LCDClient,
+  originChain: ChainId,
+  originAsset: Uint8Array
+) {
+  const result: { address: string } = await client.wasm.contractQuery(tokenBridgeAddress, {
+    wrapped_registry: {
+      chain: originChain,
+      address: fromUint8Array(originAsset),
+    },
+  });
+  return result.address;
+}
+
+/**
+ * Returns a foreign asset address on Solana for a provided native chain and asset address
+ * @param connection
+ * @param tokenBridgeAddress
+ * @param originChain
+ * @param originAsset zero pad to 32 bytes
+ * @returns
+ */
+export async function getForeignAssetSol(
+  connection: Connection,
+  tokenBridgeAddress: string,
+  originChain: ChainId,
+  originAsset: Uint8Array
+) {
+  const { wrapped_address } = await import("../solana/nft/nft_bridge");
+  const wrappedAddress = wrapped_address(
+    tokenBridgeAddress,
+    originAsset,
+    originChain
+  );
+  const wrappedAddressPK = new PublicKey(wrappedAddress);
+  const wrappedAssetAccountInfo = await connection.getAccountInfo(
+    wrappedAddressPK
+  );
+  return wrappedAssetAccountInfo ? wrappedAddressPK.toString() : null;
+}

+ 54 - 0
sdk/js/src/nft_bridge/getIsWrappedAsset.ts

@@ -0,0 +1,54 @@
+import { Connection, PublicKey } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { Bridge__factory } from "../ethers-contracts";
+import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
+
+/**
+ * Returns whether or not an asset address on Ethereum is a wormhole wrapped asset
+ * @param tokenBridgeAddress
+ * @param provider
+ * @param assetAddress
+ * @returns
+ */
+export async function getIsWrappedAssetEth(
+  tokenBridgeAddress: string,
+  provider: ethers.providers.Web3Provider,
+  assetAddress: string
+) {
+  if (!assetAddress) return false;
+  const tokenBridge = Bridge__factory.connect(tokenBridgeAddress, provider);
+  return await tokenBridge.isWrappedAsset(assetAddress);
+}
+
+export async function getIsWrappedAssetTerra(
+  tokenBridgeAddress: string,
+  wallet: TerraConnectedWallet,
+  assetAddress: string
+) {
+  return false;
+}
+
+/**
+ * Returns whether or not an asset on Solana is a wormhole wrapped asset
+ * @param connection
+ * @param tokenBridgeAddress
+ * @param mintAddress
+ * @returns
+ */
+export async function getIsWrappedAssetSol(
+  connection: Connection,
+  tokenBridgeAddress: string,
+  mintAddress: string
+) {
+  if (!mintAddress) return false;
+  const { wrapped_meta_address } = await import("../solana/nft/nft_bridge");
+  const wrappedMetaAddress = wrapped_meta_address(
+    tokenBridgeAddress,
+    new PublicKey(mintAddress).toBytes()
+  );
+  const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress);
+  const wrappedMetaAccountInfo = await connection.getAccountInfo(
+    wrappedMetaAddressPK
+  );
+  return !!wrappedMetaAccountInfo;
+}

+ 103 - 0
sdk/js/src/nft_bridge/getOriginalAsset.ts

@@ -0,0 +1,103 @@
+import { Connection, PublicKey } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { arrayify } from "ethers/lib/utils";
+import { TokenImplementation__factory } from "../ethers-contracts";
+import { ChainId, CHAIN_ID_ETH, CHAIN_ID_SOLANA, CHAIN_ID_TERRA } from "../utils";
+import { getIsWrappedAssetEth } from "./getIsWrappedAsset";
+import { ConnectedWallet as TerraConnectedWallet } from "@terra-money/wallet-provider";
+
+export interface WormholeWrappedInfo {
+  isWrapped: boolean;
+  chainId: ChainId;
+  assetAddress: Uint8Array;
+}
+
+/**
+ * Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address
+ * @param tokenBridgeAddress
+ * @param provider
+ * @param wrappedAddress
+ * @returns
+ */
+export async function getOriginalAssetEth(
+  tokenBridgeAddress: string,
+  provider: ethers.providers.Web3Provider,
+  wrappedAddress: string
+): Promise<WormholeWrappedInfo> {
+  const isWrapped = await getIsWrappedAssetEth(
+    tokenBridgeAddress,
+    provider,
+    wrappedAddress
+  );
+  if (isWrapped) {
+    const token = TokenImplementation__factory.connect(
+      wrappedAddress,
+      provider
+    );
+    const chainId = (await token.chainId()) as ChainId; // origin chain
+    const assetAddress = await token.nativeContract(); // origin address
+    return {
+      isWrapped: true,
+      chainId,
+      assetAddress: arrayify(assetAddress),
+    };
+  }
+  return {
+    isWrapped: false,
+    chainId: CHAIN_ID_ETH,
+    assetAddress: arrayify(wrappedAddress),
+  };
+}
+
+export async function getOriginalAssetTerra(
+  tokenBridgeAddress: string,
+  wallet: TerraConnectedWallet,
+  wrappedAddress: string
+): Promise<WormholeWrappedInfo> {
+  return {
+    isWrapped: false,
+    chainId: CHAIN_ID_TERRA,
+    assetAddress: arrayify(""),
+  };
+}
+
+/**
+ * Returns a origin chain and asset address on {originChain} for a provided Wormhole wrapped address
+ * @param connection
+ * @param tokenBridgeAddress
+ * @param mintAddress
+ * @returns
+ */
+export async function getOriginalAssetSol(
+  connection: Connection,
+  tokenBridgeAddress: string,
+  mintAddress: string
+): Promise<WormholeWrappedInfo> {
+  if (mintAddress) {
+    // TODO: share some of this with getIsWrappedAssetSol, like a getWrappedMetaAccountAddress or something
+    const { parse_wrapped_meta, wrapped_meta_address } = await import(
+      "../solana/nft/nft_bridge"
+    );
+    const wrappedMetaAddress = wrapped_meta_address(
+      tokenBridgeAddress,
+      new PublicKey(mintAddress).toBytes()
+    );
+    const wrappedMetaAddressPK = new PublicKey(wrappedMetaAddress);
+    const wrappedMetaAccountInfo = await connection.getAccountInfo(
+      wrappedMetaAddressPK
+    );
+    if (wrappedMetaAccountInfo) {
+      const parsed = parse_wrapped_meta(wrappedMetaAccountInfo.data);
+      return {
+        isWrapped: true,
+        chainId: parsed.chain,
+        assetAddress: parsed.token_address,
+      };
+    }
+  }
+  return {
+    isWrapped: false,
+    chainId: CHAIN_ID_SOLANA,
+    assetAddress: new Uint8Array(32),
+  };
+}

+ 5 - 0
sdk/js/src/nft_bridge/index.ts

@@ -0,0 +1,5 @@
+export * from "./getForeignAsset";
+export * from "./getIsWrappedAsset";
+export * from "./getOriginalAsset";
+export * from "./redeem";
+export * from "./transfer";

+ 95 - 0
sdk/js/src/nft_bridge/redeem.ts

@@ -0,0 +1,95 @@
+import {
+  ASSOCIATED_TOKEN_PROGRAM_ID,
+  Token,
+  TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
+import { Connection, PublicKey, Transaction } from "@solana/web3.js";
+import { ethers } from "ethers";
+import { Bridge__factory } from "../ethers-contracts";
+import { ixFromRust } from "../solana";
+
+export async function redeemOnEth(
+  tokenBridgeAddress: string,
+  signer: ethers.Signer,
+  signedVAA: Uint8Array
+) {
+  const bridge = Bridge__factory.connect(tokenBridgeAddress, signer);
+  const v = await bridge.completeTransfer(signedVAA);
+  const receipt = await v.wait();
+  return receipt;
+}
+
+export async function redeemOnSolana(
+  connection: Connection,
+  bridgeAddress: string,
+  tokenBridgeAddress: string,
+  payerAddress: string,
+  signedVAA: Uint8Array,
+  isSolanaNative: boolean,
+  mintAddress?: string // TODO: read the signedVAA and create the account if it doesn't exist
+) {
+  // TODO: this gets the target account off the vaa, but is there a way to do this via wasm?
+  // also, would this always be safe to do?
+  // should we rely on this function to create accounts at all?
+  // const { parse_vaa } = await import("../solana/core/bridge")
+  // const parsedVAA = parse_vaa(signedVAA);
+  // const targetAddress = new PublicKey(parsedVAA.payload.slice(67, 67 + 32)).toString()
+  const { complete_transfer_wrapped_ix, complete_transfer_native_ix } =
+    await import("../solana/nft/nft_bridge");
+  const ixs = [];
+  if (isSolanaNative) {
+    ixs.push(
+      ixFromRust(
+        complete_transfer_native_ix(
+          tokenBridgeAddress,
+          bridgeAddress,
+          payerAddress,
+          signedVAA
+        )
+      )
+    );
+  } else {
+    // TODO: we should always do this, they could buy wrapped somewhere else and transfer it back for the first time, but again, do it based on vaa
+    if (mintAddress) {
+      const mintPublicKey = new PublicKey(mintAddress);
+      // TODO: re: todo above, this should be swapped for the address from the vaa (may not be the same as the payer)
+      const payerPublicKey = new PublicKey(payerAddress);
+      const associatedAddress = await Token.getAssociatedTokenAddress(
+        ASSOCIATED_TOKEN_PROGRAM_ID,
+        TOKEN_PROGRAM_ID,
+        mintPublicKey,
+        payerPublicKey
+      );
+      const associatedAddressInfo = await connection.getAccountInfo(
+        associatedAddress
+      );
+      if (!associatedAddressInfo) {
+        ixs.push(
+          await Token.createAssociatedTokenAccountInstruction(
+            ASSOCIATED_TOKEN_PROGRAM_ID,
+            TOKEN_PROGRAM_ID,
+            mintPublicKey,
+            associatedAddress,
+            payerPublicKey, // owner
+            payerPublicKey // payer
+          )
+        );
+      }
+    }
+    ixs.push(
+      ixFromRust(
+        complete_transfer_wrapped_ix(
+          tokenBridgeAddress,
+          bridgeAddress,
+          payerAddress,
+          signedVAA
+        )
+      )
+    );
+  }
+  const transaction = new Transaction().add(...ixs);
+  const { blockhash } = await connection.getRecentBlockhash();
+  transaction.recentBlockhash = blockhash;
+  transaction.feePayer = new PublicKey(payerAddress);
+  return transaction;
+}

+ 104 - 0
sdk/js/src/nft_bridge/transfer.ts

@@ -0,0 +1,104 @@
+import {Token, TOKEN_PROGRAM_ID} from "@solana/spl-token";
+import {Connection, Keypair, PublicKey, Transaction} from "@solana/web3.js";
+import {ethers} from "ethers";
+import {
+    NFTBridge__factory,
+    NFTImplementation__factory,
+} from "../ethers-contracts";
+import {getBridgeFeeIx, ixFromRust} from "../solana";
+import {ChainId, CHAIN_ID_SOLANA, createNonce} from "../utils";
+
+export async function transferFromEth(
+    tokenBridgeAddress: string,
+    signer: ethers.Signer,
+    tokenAddress: string,
+    tokenID: ethers.BigNumberish,
+    recipientChain: ChainId,
+    recipientAddress: Uint8Array
+) {
+    //TODO: should we check if token attestation exists on the target chain
+    const token = NFTImplementation__factory.connect(tokenAddress, signer);
+    await token.approve(tokenBridgeAddress, tokenID);
+    const bridge = NFTBridge__factory.connect(tokenBridgeAddress, signer);
+    const v = await bridge.transferNFT(
+        tokenAddress,
+        tokenID,
+        recipientChain,
+        recipientAddress,
+        createNonce()
+    );
+    const receipt = await v.wait();
+    return receipt;
+}
+
+export async function transferFromSolana(
+    connection: Connection,
+    bridgeAddress: string,
+    tokenBridgeAddress: string,
+    payerAddress: string,
+    fromAddress: string,
+    mintAddress: string,
+    targetAddress: Uint8Array,
+    targetChain: ChainId,
+    originAddress?: Uint8Array,
+    originChain?: ChainId
+) {
+    const nonce = createNonce().readUInt32LE(0);
+    const transferIx = await getBridgeFeeIx(
+        connection,
+        bridgeAddress,
+        payerAddress
+    );
+    const {
+        transfer_native_ix,
+        transfer_wrapped_ix,
+        approval_authority_address,
+    } = await import("../solana/nft/nft_bridge");
+    const approvalIx = Token.createApproveInstruction(
+        TOKEN_PROGRAM_ID,
+        new PublicKey(fromAddress),
+        new PublicKey(approval_authority_address(tokenBridgeAddress)),
+        new PublicKey(payerAddress),
+        [],
+        Number(1)
+    );
+    let messageKey = Keypair.generate();
+    const isSolanaNative =
+        originChain === undefined || originChain === CHAIN_ID_SOLANA;
+    if (!isSolanaNative && !originAddress) {
+        throw new Error("originAddress is required when specifying originChain");
+    }
+    const ix = ixFromRust(
+        isSolanaNative
+            ? transfer_native_ix(
+                tokenBridgeAddress,
+                bridgeAddress,
+                payerAddress,
+                messageKey.publicKey.toString(),
+                fromAddress,
+                mintAddress,
+                nonce,
+                targetAddress,
+                targetChain
+            )
+            : transfer_wrapped_ix(
+                tokenBridgeAddress,
+                bridgeAddress,
+                payerAddress,
+                messageKey.publicKey.toString(),
+                fromAddress,
+                payerAddress,
+                originChain as number, // checked by isSolanaNative
+                originAddress as Uint8Array, // checked by throw
+                nonce,
+                targetAddress,
+                targetChain
+            )
+    );
+    const transaction = new Transaction().add(transferIx, approvalIx, ix);
+    const {blockhash} = await connection.getRecentBlockhash();
+    transaction.recentBlockhash = blockhash;
+    transaction.feePayer = new PublicKey(payerAddress);
+    transaction.partialSign(messageKey);
+    return transaction;
+}