sdk/js: normalize all sui addresses and types being compared

This commit is contained in:
heyitaki 2023-05-17 18:29:02 +00:00 committed by aki
parent 2402e2fbcf
commit 7608b2b740
11 changed files with 104 additions and 120 deletions

View File

@ -1,11 +0,0 @@
import { unnormalizeSuiAddress } from "./utils";
describe("Sui utils tests", () => {
test("Test unnormalizeSuiAddress", () => {
const initial =
"0x09bc8dd67bbbf59a43a9081d7166f9b41740c3a8ae868c4902d30eb247292ba4::coin::COIN";
const expected =
"0x9bc8dd67bbbf59a43a9081d7166f9b41740c3a8ae868c4902d30eb247292ba4::coin::COIN";
expect(unnormalizeSuiAddress(initial)).toBe(expected);
});
});

View File

@ -10,10 +10,10 @@ import {
SuiTransactionBlockResponse,
TransactionBlock,
} from "@mysten/sui.js";
import { DynamicFieldPage } from "@mysten/sui.js/dist/types/dynamic_fields";
import { ensureHexPrefix } from "../utils";
import { SuiRpcValidationError } from "./error";
import { SuiError } from "./types";
import { DynamicFieldPage } from "@mysten/sui.js/dist/types/dynamic_fields";
const MAX_PURE_ARGUMENT_SIZE = 16 * 1024;
const UPGRADE_CAP_TYPE = "0x2::package::UpgradeCap";
@ -46,16 +46,18 @@ export const getEmitterAddressAndSequenceFromResponseSui = (
response: SuiTransactionBlockResponse
): { emitterAddress: string; sequence: string } => {
const wormholeMessageEventType = `${originalCoreBridgePackageId}::publish_message::WormholeMessage`;
const event = response.events?.find(
(e) => e.type === wormholeMessageEventType
const event = response.events?.find((e) =>
isSameType(e.type, wormholeMessageEventType)
);
if (event === undefined) {
throw new Error(`${wormholeMessageEventType} event type not found`);
}
const { sender, sequence } = event.parsedJson || {};
if (sender === undefined || sequence === undefined) {
throw new Error("Can't find sender or sequence");
}
return { emitterAddress: sender.substring(2), sequence };
};
@ -109,7 +111,7 @@ export const getOwnedObjectId = async (
type: string
): Promise<string | null> => {
// Upgrade caps are a special case
if (normalizeSuiType(type) === normalizeSuiType(UPGRADE_CAP_TYPE)) {
if (isSameType(type, UPGRADE_CAP_TYPE)) {
throw new Error(
"`getOwnedObjectId` should not be used to get the object ID of an `UpgradeCap`. Use `getUpgradeCapObjectId` instead."
);
@ -168,8 +170,7 @@ export const getOwnedObjectIdPaginated = async (
throw new SuiRpcValidationError(res);
}
const object = res.data.find((d) => d.data?.type === type);
const object = res.data.find((d) => isSameType(d.data?.type || "", type));
if (!object && res.hasNextPage) {
return getOwnedObjectIdPaginated(
provider,
@ -208,11 +209,13 @@ export async function getPackageId(
if (!currentPackage) {
throw new Error("CurrentPackage not found");
}
const fields = await getObjectFields(provider, currentPackage.objectId);
const packageId = fields?.value?.fields?.package;
if (!packageId) {
throw new Error("Unable to get current package");
}
return packageId;
}
@ -225,7 +228,7 @@ export const getPackageIdFromType = (type: string): string | null => {
export const getTableKeyType = (tableType: string): string | null => {
if (!tableType) return null;
const match = tableType.match(/0x2::table::Table<(.*)>/);
const match = trimSuiType(tableType).match(/0x2::table::Table<(.*)>/);
if (!match) return null;
const [keyType] = match[1].split(",");
if (!isValidSuiType(keyType)) return null;
@ -276,9 +279,7 @@ export const getTokenCoinType = async (
);
}
const fields = getFieldsFromObjectResponse(response);
return fields?.value
? unnormalizeSuiAddress(ensureHexPrefix(fields.value))
: null;
return fields?.value ? trimSuiType(ensureHexPrefix(fields.value)) : null;
};
export const getTokenFromTokenRegistry = async (
@ -343,7 +344,7 @@ export const getUpgradeCapObjectId = async (
): Promise<string | null> => {
const res = await provider.getOwnedObjects({
owner,
filter: { StructType: UPGRADE_CAP_TYPE },
filter: { StructType: padSuiType(UPGRADE_CAP_TYPE) },
options: {
showContent: true,
},
@ -356,7 +357,8 @@ export const getUpgradeCapObjectId = async (
(o) =>
o.data?.objectId &&
o.data?.content?.dataType === "moveObject" &&
o.data?.content?.fields?.package === packageId
normalizeSuiAddress(o.data?.content?.fields?.package) ===
normalizeSuiAddress(packageId)
);
if (objects.length === 1) {
// We've found the object we're looking for
@ -392,7 +394,7 @@ export const getWrappedCoinType = (coinPackageId: string): string => {
export const isSameType = (a: string, b: string) => {
try {
return normalizeSuiType(a) === normalizeSuiType(b);
return trimSuiType(a) === trimSuiType(b);
} catch (e) {
return false;
}
@ -423,7 +425,13 @@ export const isValidSuiType = (type: string): boolean => {
return isValidSuiAddress(tokens[0]) && !!tokens[1] && !!tokens[2];
};
export const normalizeSuiType = (type: string): string => {
/**
* Unlike `trimSuiType`, this method does not modify nested types, it just pads
* the top-level type.
* @param type
* @returns
*/
export const padSuiType = (type: string): string => {
const tokens = type.split("::");
if (tokens.length < 3 || !isValidSuiAddress(tokens[0])) {
throw new Error(`Invalid Sui type: ${type}`);
@ -433,8 +441,8 @@ export const normalizeSuiType = (type: string): string => {
};
/**
* This method removes leading zeroes for types, as we found some getDynamicFieldObject
* value types to be stripped of leading zeroes
* This method removes leading zeroes for types in order to normalize them
* since some types returned from the RPC have leading zeroes and others don't.
*/
export const unnormalizeSuiAddress = (type: string): string =>
type.replace(/^(0x)(0*)/, "0x");
export const trimSuiType = (type: string): string =>
type.replace(/(0x)(0*)/g, "0x");

View File

@ -424,7 +424,7 @@ describe("Sui SDK tests", () => {
)
).toBe(true);
});
test.only("Transfer non-SUI Sui token to Ethereum and back", async () => {
test("Transfer non-SUI Sui token to Ethereum and back", async () => {
// Get COIN_8 coin type
const res = await suiProvider.getOwnedObjects({
owner: suiAddress,

View File

@ -15,21 +15,21 @@ import { MsgExecuteContract } from "@terra-money/terra.js";
import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
import {
Algodv2,
OnApplicationComplete,
SuggestedParams,
bigIntToBytes,
decodeAddress,
getApplicationAddress,
makeApplicationCallTxnFromObject,
makePaymentTxnWithSuggestedParamsFromObject,
OnApplicationComplete,
SuggestedParams,
} from "algosdk";
import { Types } from "aptos";
import BN from "bn.js";
import { ethers, PayableOverrides } from "ethers";
import { PayableOverrides, ethers } from "ethers";
import { FunctionCallOptions } from "near-api-js/lib/account";
import { Provider } from "near-api-js/lib/providers";
import { getIsWrappedAssetNear } from ".";
import { getMessageFee, optin, TransactionSignerPair } from "../algorand";
import { TransactionSignerPair, getMessageFee, optin } from "../algorand";
import { attestToken as attestTokenAptos } from "../aptos";
import { isNativeDenomXpla } from "../cosmwasm";
import { Bridge__factory } from "../ethers-contracts";
@ -38,8 +38,8 @@ import { createAttestTokenInstruction } from "../solana/tokenBridge";
import { getPackageId } from "../sui/utils";
import { isNativeDenom } from "../terra";
import {
callFunctionNear,
ChainId,
callFunctionNear,
hashAccount,
textToHexString,
textToUint8Array,
@ -324,14 +324,11 @@ export async function attestFromSui(
if (metadata === null || metadata.id === null) {
throw new Error(`Coin metadata ID for type ${coinType} not found`);
}
const coreBridgePackageId = await getPackageId(
provider,
coreBridgeStateObjectId
);
const tokenBridgePackageId = await getPackageId(
provider,
tokenBridgeStateObjectId
);
const [coreBridgePackageId, tokenBridgePackageId] = await Promise.all([
getPackageId(provider, coreBridgeStateObjectId),
getPackageId(provider, tokenBridgeStateObjectId),
]);
const tx = new TransactionBlock();
const [feeCoin] = tx.splitCoins(tx.gas, [tx.pure(feeAmount)]);
const [messageTicket] = tx.moveCall({

View File

@ -15,7 +15,7 @@ import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
import { Algodv2 } from "algosdk";
import { Types } from "aptos";
import BN from "bn.js";
import { ethers, Overrides } from "ethers";
import { Overrides, ethers } from "ethers";
import { fromUint8Array } from "js-base64";
import { FunctionCallOptions } from "near-api-js/lib/account";
import { Provider } from "near-api-js/lib/providers";
@ -196,20 +196,10 @@ export async function createWrappedOnSui(
wrappedAssetSetupType: string,
attestVAA: Uint8Array
): Promise<TransactionBlock> {
// WrappedAssetSetup looks like
// 0x92d81f28c167d90f84638c654b412fe7fa8e55bdfac7f638bdcf70306289be86::create_wrapped::WrappedAssetSetup<0xa40e0511f7d6531dd2dfac0512c7fd4a874b76f5994985fb17ee04501a2bb050::coin::COIN, 0x4eb7c5bca3759ab3064b46044edb5668c9066be8a543b28b58375f041f876a80::version_control::V__0_1_1>
// ugh
const versionType = wrappedAssetSetupType.split(", ")[1].replace(">", "");
const coreBridgePackageId = await getPackageId(
provider,
coreBridgeStateObjectId
);
const tokenBridgePackageId = await getPackageId(
provider,
tokenBridgeStateObjectId
);
const [coreBridgePackageId, tokenBridgePackageId] = await Promise.all([
getPackageId(provider, coreBridgeStateObjectId),
getPackageId(provider, tokenBridgeStateObjectId),
]);
// Get coin metadata
const coinType = getWrappedCoinType(coinPackageId);
@ -221,6 +211,8 @@ export async function createWrappedOnSui(
);
}
// WrappedAssetSetup looks like
// 0x92d81f28c167d90f84638c654b412fe7fa8e55bdfac7f638bdcf70306289be86::create_wrapped::WrappedAssetSetup<0xa40e0511f7d6531dd2dfac0512c7fd4a874b76f5994985fb17ee04501a2bb050::coin::COIN, 0x4eb7c5bca3759ab3064b46044edb5668c9066be8a543b28b58375f041f876a80::version_control::V__0_1_1>
const wrappedAssetSetupObjectId = await getOwnedObjectId(
provider,
signerAddress,
@ -258,6 +250,7 @@ export async function createWrappedOnSui(
});
// Construct complete registration payload
const versionType = wrappedAssetSetupType.split(", ")[1].replace(">", ""); // ugh
tx.moveCall({
target: `${tokenBridgePackageId}::create_wrapped::complete_registration`,
arguments: [

View File

@ -9,12 +9,12 @@ import { ethers } from "ethers";
import { fromUint8Array } from "js-base64";
import { Provider } from "near-api-js/lib/providers";
import { redeemOnTerra } from ".";
import { ensureHexPrefix, TERRA_REDEEMED_CHECK_WALLET_ADDRESS } from "..";
import { TERRA_REDEEMED_CHECK_WALLET_ADDRESS, ensureHexPrefix } from "..";
import {
BITS_PER_KEY,
calcLogicSigAccount,
MAX_BITS,
_parseVAAAlgorand,
calcLogicSigAccount,
} from "../algorand";
import { TokenBridgeState } from "../aptos/types";
import { getSignedVAAHash } from "../bridge";
@ -23,7 +23,7 @@ import { getClaim } from "../solana/wormhole";
import { getObjectFields, getTableKeyType } from "../sui/utils";
import { safeBigIntToNumber } from "../utils/bigint";
import { callFunctionNear } from "../utils/near";
import { parseVaa, SignedVaa } from "../vaa/wormhole";
import { SignedVaa, parseVaa } from "../vaa/wormhole";
export async function getIsTransferCompletedEth(
tokenBridgeAddress: string,
@ -280,15 +280,18 @@ export async function getIsTransferCompletedSui(
if (!tokenBridgeStateFields) {
throw new Error("Unable to fetch object fields from token bridge state");
}
const hashes = tokenBridgeStateFields.consumed_vaas?.fields?.hashes;
const tableObjectId = hashes?.fields?.items?.fields?.id?.id;
if (!tableObjectId) {
throw new Error("Unable to fetch consumed VAAs table");
}
const keyType = getTableKeyType(hashes?.fields?.items?.type);
if (!keyType) {
throw new Error("Unable to get key type");
}
const hash = getSignedVAAHash(transferVAA);
const response = await provider.getDynamicFieldObject({
parentId: tableObjectId,
@ -302,9 +305,11 @@ export async function getIsTransferCompletedSui(
if (!response.error) {
return true;
}
if (response.error.code === "dynamicFieldNotFound") {
return false;
}
throw new Error(
`Unexpected getDynamicFieldObject response ${response.error}`
);

View File

@ -120,11 +120,12 @@ export async function getIsWrappedAssetSui(
tokenBridgeStateObjectId: string,
type: string
): Promise<boolean> {
// // An easy way to determine if given asset isn't a wrapped asset is to ensure
// // module name and struct name are coin and COIN respectively.
// if (!type.endsWith("::coin::COIN")) {
// return false;
// }
// An easy way to determine if given asset isn't a wrapped asset is to ensure
// module name and struct name are coin and COIN respectively.
if (!type.endsWith("::coin::COIN")) {
return false;
}
const response = await getTokenFromTokenRegistry(
provider,
tokenBridgeStateObjectId,
@ -133,9 +134,11 @@ export async function getIsWrappedAssetSui(
if (!response.error) {
return response.data?.type?.includes("WrappedAsset") || false;
}
if (response.error.code === "dynamicFieldNotFound") {
return false;
}
throw new Error(
`Unexpected getDynamicFieldObject response ${response.error}`
);

View File

@ -23,24 +23,24 @@ import {
getFieldsFromObjectResponse,
getTokenFromTokenRegistry,
isValidSuiType,
unnormalizeSuiAddress,
trimSuiType,
} from "../sui";
import { buildNativeId } from "../terra";
import {
assertChain,
callFunctionNear,
ChainId,
ChainName,
CHAIN_ID_ALGORAND,
CHAIN_ID_APTOS,
CHAIN_ID_NEAR,
CHAIN_ID_SOLANA,
CHAIN_ID_SUI,
CHAIN_ID_TERRA,
coalesceChainId,
coalesceCosmWasmChainId,
ChainId,
ChainName,
CosmWasmChainId,
CosmWasmChainName,
assertChain,
callFunctionNear,
coalesceChainId,
coalesceCosmWasmChainId,
hexToUint8Array,
isValidAptosType,
} from "../utils";
@ -347,12 +347,14 @@ export async function getOriginalAssetSui(
);
}
if (
fields.value.type.includes(`wrapped_asset::WrappedAsset<${coinType}>`) ||
fields.value.type.includes(
`wrapped_asset::WrappedAsset<${unnormalizeSuiAddress(coinType)}>`
)
) {
// Normalize types
const type = trimSuiType(fields.value.type);
coinType = trimSuiType(coinType);
// Check if wrapped or native asset. We check inclusion instead of equality
// because it saves us from making an additional RPC call to fetch the
// package ID.
if (type.includes(`wrapped_asset::WrappedAsset<${coinType}>`)) {
return {
isWrapped: true,
chainId: Number(fields.value.fields.info.fields.token_chain) as ChainId,
@ -360,12 +362,7 @@ export async function getOriginalAssetSui(
fields.value.fields.info.fields.token_address.fields.value.fields.data
),
};
} else if (
fields.value.type.includes(`native_asset::NativeAsset<${coinType}>`) ||
fields.value.type.includes(
`native_asset::NativeAsset<${unnormalizeSuiAddress(coinType)}>`
)
) {
} else if (type.includes(`native_asset::NativeAsset<${coinType}>`)) {
return {
isWrapped: false,
chainId: CHAIN_ID_SUI,

View File

@ -379,14 +379,11 @@ export async function redeemOnSui(
if (!coinType) {
throw new Error("Unable to fetch token coinType");
}
const coreBridgePackageId = await getPackageId(
provider,
coreBridgeStateObjectId
);
const tokenBridgePackageId = await getPackageId(
provider,
tokenBridgeStateObjectId
);
const [coreBridgePackageId, tokenBridgePackageId] = await Promise.all([
getPackageId(provider, coreBridgeStateObjectId),
getPackageId(provider, tokenBridgeStateObjectId),
]);
const tx = new TransactionBlock();
const [verifiedVAA] = tx.moveCall({
target: `${coreBridgePackageId}::vaa::parse_and_verify`,

View File

@ -6,11 +6,11 @@ import {
} from "@mysten/sui.js";
import {
ACCOUNT_SIZE,
NATIVE_MINT,
TOKEN_PROGRAM_ID,
createCloseAccountInstruction,
createInitializeAccountInstruction,
getMinimumBalanceForRentExemptAccount,
NATIVE_MINT,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
Commitment,
@ -18,33 +18,33 @@ import {
Keypair,
PublicKey,
PublicKeyInitData,
SystemProgram,
Transaction as SolanaTransaction,
SystemProgram,
} from "@solana/web3.js";
import { MsgExecuteContract } from "@terra-money/terra.js";
import { MsgExecuteContract as XplaMsgExecuteContract } from "@xpla/xpla.js";
import {
Algodv2,
Transaction as AlgorandTransaction,
OnApplicationComplete,
SuggestedParams,
bigIntToBytes,
getApplicationAddress,
makeApplicationCallTxnFromObject,
makeAssetTransferTxnWithSuggestedParamsFromObject,
makePaymentTxnWithSuggestedParamsFromObject,
OnApplicationComplete,
SuggestedParams,
Transaction as AlgorandTransaction,
} from "algosdk";
import { Types } from "aptos";
import BN from "bn.js";
import { ethers, Overrides, PayableOverrides } from "ethers";
import { Overrides, PayableOverrides, ethers } from "ethers";
import { FunctionCallOptions } from "near-api-js/lib/account";
import { Provider } from "near-api-js/lib/providers";
import { getIsWrappedAssetNear } from "..";
import {
TransactionSignerPair,
assetOptinCheck,
getMessageFee,
optin,
TransactionSignerPair,
} from "../algorand";
import {
transferTokens as transferTokensAptos,
@ -67,10 +67,10 @@ import { getPackageId, isSameType } from "../sui";
import { SuiCoinObject } from "../sui/types";
import { isNativeDenom } from "../terra";
import {
callFunctionNear,
CHAIN_ID_SOLANA,
ChainId,
ChainName,
CHAIN_ID_SOLANA,
callFunctionNear,
coalesceChainId,
createNonce,
hexToUint8Array,
@ -938,6 +938,7 @@ export async function transferFromSui(
if (payload !== null) {
throw new Error("Sui transfer with payload not implemented");
}
const [primaryCoin, ...mergeCoins] = coins.filter((coin) =>
isSameType(coin.coinType, coinType)
);
@ -946,14 +947,11 @@ export async function transferFromSui(
`Coins array doesn't contain any coins of type ${coinType}`
);
}
const coreBridgePackageId = await getPackageId(
provider,
coreBridgeStateObjectId
);
const tokenBridgePackageId = await getPackageId(
provider,
tokenBridgeStateObjectId
);
const [coreBridgePackageId, tokenBridgePackageId] = await Promise.all([
getPackageId(provider, coreBridgeStateObjectId),
getPackageId(provider, tokenBridgeStateObjectId),
]);
const tx = new TransactionBlock();
const [transferCoin] = (() => {
if (coinType === SUI_TYPE_ARG) {
@ -966,6 +964,7 @@ export async function transferFromSui(
mergeCoins.map((coin) => tx.object(coin.coinObjectId))
);
}
return tx.splitCoins(primaryCoinInput, [tx.pure(amount)]);
}
})();

View File

@ -3,7 +3,7 @@ import {
SUI_CLOCK_OBJECT_ID,
TransactionBlock,
} from "@mysten/sui.js";
import { ethers, Overrides } from "ethers";
import { Overrides, ethers } from "ethers";
import {
createWrappedOnAlgorand,
createWrappedOnAptos,
@ -46,14 +46,10 @@ export async function updateWrappedOnSui(
coinPackageId: string,
attestVAA: Uint8Array
): Promise<TransactionBlock> {
const coreBridgePackageId = await getPackageId(
provider,
coreBridgeStateObjectId
);
const tokenBridgePackageId = await getPackageId(
provider,
tokenBridgeStateObjectId
);
const [coreBridgePackageId, tokenBridgePackageId] = await Promise.all([
getPackageId(provider, coreBridgeStateObjectId),
getPackageId(provider, tokenBridgeStateObjectId),
]);
// Get coin metadata
const coinType = getWrappedCoinType(coinPackageId);