sdk/js: add tests for aptos sdk

This commit is contained in:
heyitaki 2023-01-19 07:21:52 +00:00 committed by Evan Gray
parent 7c1721d199
commit 3bb0562f45
9 changed files with 346 additions and 159 deletions

View File

@ -3,6 +3,7 @@
"^.+\\.(t|j)sx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"testPathIgnorePatterns": ["__tests__/utils"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "node"],
"testTimeout": 60000
}

View File

@ -0,0 +1,178 @@
import { beforeAll, describe, expect, jest, test } from "@jest/globals";
import { AptosAccount, AptosClient, FaucetClient, Types } from "aptos";
import { ethers } from "ethers";
import Web3 from "web3";
import {
CHAIN_ID_APTOS,
CHAIN_ID_ETH,
CONTRACTS,
deriveResourceAccountAddress,
generateSignAndSubmitEntryFunction,
tryNativeToUint8Array,
} from "../../utils";
import { parseNftTransferVaa } from "../../vaa";
import { getIsTransferCompletedAptos } from "../getIsTransferCompleted";
import { getIsWrappedAssetAptos } from "../getIsWrappedAsset";
import { getOriginalAssetAptos } from "../getOriginalAsset";
import { redeemOnAptos } from "../redeem";
import { transferFromAptos, transferFromEth } from "../transfer";
import {
APTOS_FAUCET_URL,
APTOS_NODE_URL,
ETH_NODE_URL,
ETH_PRIVATE_KEY,
} from "./consts";
import { deployTestNftOnEthereum } from "./utils/deployTestNft";
import { getSignedVaaAptos, getSignedVaaEthereum } from "./utils/getSignedVaa";
jest.setTimeout(60000);
const APTOS_NFT_BRIDGE_ADDRESS = CONTRACTS.DEVNET.aptos.nft_bridge;
const ETH_NFT_BRIDGE_ADDRESS = CONTRACTS.DEVNET.ethereum.nft_bridge;
let aptosClient: AptosClient;
let aptosAccount: AptosAccount;
let web3: Web3;
let ethProvider: ethers.providers.WebSocketProvider;
let ethSigner: ethers.Wallet;
beforeAll(async () => {
// aptos setup
aptosClient = new AptosClient(APTOS_NODE_URL);
aptosAccount = new AptosAccount();
const faucet = new FaucetClient(APTOS_NODE_URL, APTOS_FAUCET_URL);
await faucet.fundAccount(aptosAccount.address(), 100_000_000);
// ethereum setup
web3 = new Web3(ETH_NODE_URL);
ethProvider = new ethers.providers.WebSocketProvider(ETH_NODE_URL);
ethSigner = new ethers.Wallet(ETH_PRIVATE_KEY, ethProvider); // corresponds to accounts[1]
});
describe("Aptos NFT SDK tests", () => {
test("Transfer ERC-721 from Ethereum to Aptos and back", async () => {
const ETH_COLLECTION_NAME = "Not an APE 🐒";
// create NFT on Ethereum
const ethNft = await deployTestNftOnEthereum(
web3,
ethSigner,
ETH_COLLECTION_NAME,
"APE🐒",
"https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/",
11
);
// transfer NFT from Ethereum to Aptos
const ethTransferTx = await transferFromEth(
ETH_NFT_BRIDGE_ADDRESS,
ethSigner,
ethNft.address,
10,
CHAIN_ID_APTOS,
aptosAccount.address().toUint8Array()
);
// observe tx and get vaa
const ethTransferVaa = await getSignedVaaEthereum(ethTransferTx);
const ethTransferVaaParsed = parseNftTransferVaa(ethTransferVaa);
expect(ethTransferVaaParsed.name).toBe(ETH_COLLECTION_NAME);
// redeem NFT on Aptos
const aptosRedeemPayload = await redeemOnAptos(
APTOS_NFT_BRIDGE_ADDRESS,
ethTransferVaa
);
const aptosRedeemTx = await generateSignAndSubmitEntryFunction(
aptosClient,
aptosAccount,
aptosRedeemPayload
);
const aptosRedeemTxResult = (await aptosClient.waitForTransactionWithResult(
aptosRedeemTx.hash
)) as Types.UserTransaction;
expect(aptosRedeemTxResult.success).toBe(true);
expect(
await getIsTransferCompletedAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
ethTransferVaa
)
).toBe(true);
// get creator address
const creatorAddress = await deriveResourceAccountAddress(
APTOS_NFT_BRIDGE_ADDRESS,
CHAIN_ID_ETH,
ethNft.address
);
expect(creatorAddress).toBeTruthy();
expect(
await getIsWrappedAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
creatorAddress!
)
).toBe(true);
expect(
await getOriginalAssetAptos(
aptosClient,
APTOS_NFT_BRIDGE_ADDRESS,
creatorAddress!
)
).toMatchObject({
isWrapped: true,
chainId: CHAIN_ID_ETH,
assetAddress: Uint8Array.from(ethTransferVaaParsed.tokenAddress),
});
// transfer NFT from Aptos back to Ethereum
const aptosTransferPayload = await transferFromAptos(
APTOS_NFT_BRIDGE_ADDRESS,
creatorAddress!,
ethTransferVaaParsed.name, // TODO(aki): derive this properly
ethTransferVaaParsed.tokenId.toString(16).padStart(64, "0"), // TODO(aki): derive this properly
0,
CHAIN_ID_ETH,
tryNativeToUint8Array(ethSigner.address, CHAIN_ID_ETH)
);
const aptosTransferTx = await generateSignAndSubmitEntryFunction(
aptosClient,
aptosAccount,
aptosTransferPayload
);
const aptosTransferTxResult =
(await aptosClient.waitForTransactionWithResult(
aptosTransferTx.hash
)) as Types.UserTransaction;
expect(aptosTransferTxResult.success).toBe(true);
// observe tx and get vaa
const aptosTransferVaa = await getSignedVaaAptos(
aptosClient,
aptosTransferTxResult
);
const aptosTransferVaaParsed = parseNftTransferVaa(aptosTransferVaa);
expect(aptosTransferVaaParsed.name).toBe(ETH_COLLECTION_NAME);
expect(aptosTransferVaaParsed.tokenAddress.toString("hex")).toBe(
ethTransferVaaParsed.tokenAddress.toString("hex")
);
// TODO(aki): make this work
// // redeem NFT on Ethereum & check NFT is the same
// const ethRedeemTxResult = await redeemOnEth(
// ETH_NFT_BRIDGE_ADDRESS,
// ethSigner,
// aptosTransferVaa,
// { gasLimit: 3e7 }
// );
// console.log(
// "ethRedeemTxResult",
// JSON.stringify(ethRedeemTxResult, null, 2)
// );
// expect(ethRedeemTxResult.status).toBe(1);
});
});
function afterAll(arg0: () => Promise<void>) {
throw new Error("Function not implemented.");
}

View File

@ -33,6 +33,15 @@ export const WORMHOLE_RPC_HOSTS = ci
? ["http://guardian:7071"]
: ["http://localhost:7071"];
export const APTOS_PRIVATE_KEY =
"537c1f91e56891445b491068f519b705f8c0f1a1e66111816dd5d4aa85b8113d";
export const APTOS_NODE_URL = ci
? "http://aptos:8080/v1"
: "http://0.0.0.0:8080/v1";
export const APTOS_FAUCET_URL = ci
? "http://aptos:8081"
: "http://0.0.0.0:8081";
describe("consts should exist", () => {
it("has Solana test token", () => {
expect.assertions(1);

View File

@ -32,10 +32,7 @@ import {
SOLANA_PRIVATE_KEY,
TEST_SOLANA_TOKEN,
} from "./consts";
import {
waitUntilTransactionObservedEthereum,
waitUntilTransactionObservedSolana,
} from "./utils/waitUntilTransactionObserved";
import { getSignedVaaEthereum, getSignedVaaSolana } from "./utils/getSignedVaa";
jest.setTimeout(60000);
@ -59,72 +56,6 @@ afterEach(() => {
});
describe("Integration Tests", () => {
// TODO: figure out why this isn't working
// test("Send Ethereum ERC-721 to Solana and back", (done) => {
// (async () => {
// try {
// const erc721 = await deployNFTOnEth(
// "Not an APE 🐒",
// "APE🐒",
// "https://cloudflare-ipfs.com/ipfs/QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/",
// 11 // mint ids 0..10 (inclusive)
// );
// const sol_addr = await nft_bridge.getForeignAssetSol(
// CONTRACTS.DEVNET.solana.nft_bridge,
// CHAIN_ID_ETH,
// tryNativeToUint8Array(erc721.address, CHAIN_ID_ETH),
// arrayify(BigNumber.from("10"))
// );
// const fromAddress = await Token.getAssociatedTokenAddress(
// ASSOCIATED_TOKEN_PROGRAM_ID,
// TOKEN_PROGRAM_ID,
// new PublicKey(sol_addr),
// keypair.publicKey
// );
// const transaction = await _transferFromEth(
// erc721.address,
// 10,
// fromAddress.toString(),
// CHAIN_ID_SOLANA
// );
// let signedVAA = await waitUntilEthTxObserved(transaction);
// await _redeemOnSolana(signedVAA);
// let ownerEth = await erc721.ownerOf(10);
// expect(ownerEth).not.toBe(signer.address);
// // TODO: check wrapped balance
// // Send back the NFT to ethereum
// const transaction2 = await _transferFromSolana(
// fromAddress,
// sol_addr,
// signer.address,
// CHAIN_ID_ETH,
// tryNativeToUint8Array(erc721.address, CHAIN_ID_ETH),
// CHAIN_ID_ETH,
// arrayify(BigNumber.from("10"))
// );
// signedVAA = await waitUntilSolanaTxObserved(transaction2);
// (await expectReceivedOnEth(signedVAA)).toBe(false);
// await _redeemOnEth(signedVAA);
// (await expectReceivedOnEth(signedVAA)).toBe(true);
// // ensure that the transaction roundtrips back to the original native asset
// ownerEth = await erc721.ownerOf(10);
// expect(ownerEth).toBe(signer.address);
// // TODO: the wrapped token should no longer exist
// done();
// } catch (e) {
// console.error(e);
// done(
// `An error occured while trying to transfer from Ethereum to Solana and back: ${e}`
// );
// }
// })();
// });
test("Send Solana SPL to Ethereum and back", (done) => {
(async () => {
try {
@ -140,7 +71,7 @@ describe("Integration Tests", () => {
signer.address,
CHAIN_ID_ETH
);
let signedVAA = await waitUntilTransactionObservedSolana(transaction1);
let signedVAA = await getSignedVaaSolana(transaction1);
// we get the solana token id from the VAA
const { tokenId } = parseNftTransferVaa(signedVAA);
@ -162,7 +93,7 @@ describe("Integration Tests", () => {
fromAddress.toString(),
CHAIN_ID_SOLANA
);
signedVAA = await waitUntilTransactionObservedEthereum(transaction3);
signedVAA = await getSignedVaaEthereum(transaction3);
const { name, symbol } = parseNftTransferVaa(signedVAA);

View File

@ -1,3 +1,4 @@
import { AptosAccount, AptosClient, TokenClient } from "aptos";
import { ethers } from "ethers";
import Web3 from "web3";
import {
@ -6,6 +7,31 @@ import {
} from "../../../ethers-contracts";
const ERC721 = require("@openzeppelin/contracts/build/contracts/ERC721PresetMinterPauserAutoId.json");
export const deployTestNftOnAptos = async (
client: AptosClient,
account: AptosAccount
) => {
const tokenClient = new TokenClient(client);
const collectionName = "testCollection";
const collectionHash = await tokenClient.createCollection(
account,
collectionName,
"test collection",
"https://www.wormhole.com"
);
await client.waitForTransaction(collectionHash);
const tokenHash = await tokenClient.createToken(
account,
collectionName,
"testToken",
"test token",
1,
"https://wormhole.com/static/a9281881f58cc7fbe4df796a9ba684ac/90d9d/s2.webp"
);
await client.waitForTransaction(tokenHash);
};
export async function deployTestNftOnEthereum(
web3: Web3,
signer: ethers.Wallet,

View File

@ -0,0 +1,100 @@
import { GetSignedVAAResponse } from "@certusone/wormhole-sdk-proto-web/lib/cjs/publicrpc/v1/publicrpc";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { TransactionResponse } from "@solana/web3.js";
import { AptosClient, Types } from "aptos";
import { ethers } from "ethers";
import { NftBridgeState } from "../../../aptos/types";
import {
getEmitterAddressEth,
getEmitterAddressSolana,
parseSequenceFromLogAptos,
parseSequenceFromLogEth,
parseSequenceFromLogSolana,
} from "../../../bridge";
import { getSignedVAAWithRetry } from "../../../rpc";
import {
ChainId,
CHAIN_ID_APTOS,
CHAIN_ID_ETH,
CHAIN_ID_SOLANA,
CONTRACTS,
} from "../../../utils";
import { WORMHOLE_RPC_HOSTS } from "../consts";
// TODO(aki): implement getEmitterAddressAptos and sub here
export async function getSignedVaaAptos(
client: AptosClient,
result: Types.UserTransaction
): Promise<Uint8Array> {
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogAptos(
CONTRACTS.DEVNET.aptos.core,
result
);
if (sequence === null) {
throw new Error("aptos: Could not parse sequence from logs");
}
const nftBridgeAddress = CONTRACTS.DEVNET.aptos.nft_bridge;
const state = (await client.getAccountResources(nftBridgeAddress)).find(
(r) => r.type === `${nftBridgeAddress}::state::State`
);
if (!state) {
throw new Error("aptos: Could not find State resource");
}
// TODO: ensure 0x is stripped if exists
const emitterAddress = (
state.data as NftBridgeState
).emitter_cap.emitter.padStart(64, "0");
// poll until the guardian(s) witness and sign the vaa
return getSignedVaa(CHAIN_ID_APTOS, emitterAddress, sequence);
}
export async function getSignedVaaEthereum(
receipt: ethers.ContractReceipt
): Promise<Uint8Array> {
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogEth(
receipt,
CONTRACTS.DEVNET.ethereum.core
);
const emitterAddress = getEmitterAddressEth(
CONTRACTS.DEVNET.ethereum.nft_bridge
);
// poll until the guardian(s) witness and sign the vaa
return getSignedVaa(CHAIN_ID_ETH, emitterAddress, sequence);
}
export async function getSignedVaaSolana(
response: TransactionResponse
): Promise<Uint8Array> {
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogSolana(response);
const emitterAddress = getEmitterAddressSolana(
CONTRACTS.DEVNET.solana.nft_bridge
);
// poll until the guardian(s) witness and sign the vaa
return getSignedVaa(CHAIN_ID_SOLANA, emitterAddress, sequence);
}
const getSignedVaa = async (
chain: ChainId,
emitterAddress: string,
sequence: string
): Promise<Uint8Array> => {
const { vaaBytes: signedVAA }: GetSignedVAAResponse =
await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
chain,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
return signedVAA;
};

View File

@ -1,57 +0,0 @@
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { TransactionResponse } from "@solana/web3.js";
import { ethers } from "ethers";
import {
getEmitterAddressEth,
getEmitterAddressSolana,
parseSequenceFromLogEth,
parseSequenceFromLogSolana,
} from "../../../bridge";
import { getSignedVAAWithRetry } from "../../../rpc";
import { CHAIN_ID_ETH, CHAIN_ID_SOLANA, CONTRACTS } from "../../../utils";
import { WORMHOLE_RPC_HOSTS } from "../consts";
export async function waitUntilTransactionObservedEthereum(
receipt: ethers.ContractReceipt
): Promise<Uint8Array> {
// get the sequence from the logs (needed to fetch the vaa)
let sequence = parseSequenceFromLogEth(
receipt,
CONTRACTS.DEVNET.ethereum.core
);
let emitterAddress = getEmitterAddressEth(
CONTRACTS.DEVNET.ethereum.nft_bridge
);
// poll until the guardian(s) witness and sign the vaa
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_ETH,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
return signedVAA;
}
export async function waitUntilTransactionObservedSolana(
response: TransactionResponse
): Promise<Uint8Array> {
// get the sequence from the logs (needed to fetch the vaa)
const sequence = parseSequenceFromLogSolana(response);
const emitterAddress = await getEmitterAddressSolana(
CONTRACTS.DEVNET.solana.nft_bridge
);
// poll until the guardian(s) witness and sign the vaa
const { vaaBytes: signedVAA } = await getSignedVAAWithRetry(
WORMHOLE_RPC_HOSTS,
CHAIN_ID_SOLANA,
emitterAddress,
sequence,
{
transport: NodeHttpTransport(),
}
);
return signedVAA;
}

View File

@ -1,3 +1,4 @@
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { describe, expect, jest, test } from "@jest/globals";
import {
AptosAccount,
@ -6,6 +7,8 @@ import {
HexString,
Types,
} from "aptos";
import { ethers } from "ethers";
import { parseUnits } from "ethers/lib/utils";
import {
approveEth,
APTOS_TOKEN_BRIDGE_EMITTER_ADDRESS,
@ -17,7 +20,8 @@ import {
createWrappedOnAptos,
createWrappedOnEth,
createWrappedTypeOnAptos,
getAssetFullyQualifiedType,
generateSignAndSubmitEntryFunction,
generateSignAndSubmitScript,
getEmitterAddressEth,
getExternalAddressFromType,
getForeignAssetAptos,
@ -30,55 +34,45 @@ import {
hexToUint8Array,
redeemOnAptos,
redeemOnEth,
generateSignAndSubmitEntryFunction,
generateSignAndSubmitScript,
transferFromAptos,
transferFromEth,
tryNativeToHexString,
tryNativeToUint8Array,
uint8ArrayToHex,
} from "../..";
import { registerCoin } from "../../aptos";
import {
parseSequenceFromLogAptos,
parseSequenceFromLogEth,
} from "../../bridge/parseSequenceFromLog";
import { TokenImplementation__factory } from "../../ethers-contracts";
import {
APTOS_FAUCET_URL,
APTOS_NODE_URL,
APTOS_PRIVATE_KEY,
ETH_NODE_URL,
ETH_PRIVATE_KEY6,
TEST_ERC20,
WORMHOLE_RPC_HOSTS,
} from "./consts";
import {
parseSequenceFromLogAptos,
parseSequenceFromLogEth,
} from "../../bridge/parseSequenceFromLog";
import { NodeHttpTransport } from "@improbable-eng/grpc-web-node-http-transport";
import { ethers } from "ethers";
import { parseUnits } from "ethers/lib/utils";
import { registerCoin } from "../../aptos";
import { TokenImplementation__factory } from "../../ethers-contracts";
const JEST_TEST_TIMEOUT = 60000;
jest.setTimeout(JEST_TEST_TIMEOUT);
describe("Aptos SDK tests", () => {
test("Transfer native token from Aptos to Ethereum", async () => {
const APTOS_TOKEN_BRIDGE = CONTRACTS.DEVNET.aptos.token_bridge;
const APTOS_CORE_BRIDGE = CONTRACTS.DEVNET.aptos.core;
const COIN_TYPE = "0x1::aptos_coin::AptosCoin";
// setup aptos
const client = new AptosClient(APTOS_NODE_URL);
const sender = new AptosAccount();
const faucet = new FaucetClient(APTOS_NODE_URL, APTOS_FAUCET_URL);
const sender = new AptosAccount(hexToUint8Array(APTOS_PRIVATE_KEY));
const aptosTokenBridge = CONTRACTS.DEVNET.aptos.token_bridge;
const aptosCoreBridge = CONTRACTS.DEVNET.aptos.core;
// sanity check funds in the account
const COIN_TYPE = "0x1::aptos_coin::AptosCoin";
const before = await getBalanceAptos(client, COIN_TYPE, sender.address());
await faucet.fundAccount(sender.address(), 100_000_000);
const after = await getBalanceAptos(client, COIN_TYPE, sender.address());
expect(Number(after) - Number(before)).toEqual(100_000_000);
// attest native aptos token
const attestPayload = attestFromAptos(
aptosTokenBridge,
APTOS_TOKEN_BRIDGE,
CHAIN_ID_APTOS,
COIN_TYPE
);
@ -90,7 +84,7 @@ describe("Aptos SDK tests", () => {
await client.waitForTransaction(tx.hash);
// get signed attest vaa
let sequence = parseSequenceFromLogAptos(aptosCoreBridge, tx);
let sequence = parseSequenceFromLogAptos(APTOS_CORE_BRIDGE, tx);
expect(sequence).toBeTruthy();
const { vaaBytes: attestVAA } = await getSignedVAAWithRetry(
@ -135,7 +129,7 @@ describe("Aptos SDK tests", () => {
await getBalanceAptos(client, COIN_TYPE, sender.address())
);
const transferPayload = transferFromAptos(
aptosTokenBridge,
APTOS_TOKEN_BRIDGE,
COIN_TYPE,
(10_000_000).toString(),
CHAIN_ID_ETH,
@ -157,7 +151,7 @@ describe("Aptos SDK tests", () => {
).toBe(true);
// get signed transfer vaa
sequence = parseSequenceFromLogAptos(aptosCoreBridge, tx);
sequence = parseSequenceFromLogAptos(APTOS_CORE_BRIDGE, tx);
expect(sequence).toBeTruthy();
const { vaaBytes: transferVAA } = await getSignedVAAWithRetry(
@ -243,7 +237,9 @@ describe("Aptos SDK tests", () => {
// setup aptos
const client = new AptosClient(APTOS_NODE_URL);
const recipient = new AptosAccount(hexToUint8Array(APTOS_PRIVATE_KEY));
const recipient = new AptosAccount();
const faucet = new FaucetClient(APTOS_NODE_URL, APTOS_FAUCET_URL);
await faucet.fundAccount(recipient.address(), 100_000_000);
const aptosTokenBridge = CONTRACTS.DEVNET.aptos.token_bridge;
const createWrappedCoinTypePayload = createWrappedTypeOnAptos(
aptosTokenBridge,

View File

@ -172,7 +172,8 @@ const MAINNET = {
core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
token_bridge:
"0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
nft_bridge: "0x1bdffae984043833ed7fe223f7af7a3f8902d04129b14f801823e64827da7130",
nft_bridge:
"0x1bdffae984043833ed7fe223f7af7a3f8902d04129b14f801823e64827da7130",
},
sui: {
core: undefined,
@ -328,7 +329,8 @@ const TESTNET = {
core: "0x5bc11445584a763c1fa7ed39081f1b920954da14e04b32440cba863d03e19625",
token_bridge:
"0x576410486a2da45eee6c949c995670112ddf2fbeedab20350d506328eefc9d4f",
nft_bridge: undefined,
nft_bridge:
"0x1bdffae984043833ed7fe223f7af7a3f8902d04129b14f801823e64827da7130",
},
sui: {
core: undefined,
@ -484,7 +486,8 @@ const DEVNET = {
core: "0xde0036a9600559e295d5f6802ef6f3f802f510366e0c23912b0655d972166017",
token_bridge:
"0x84a5f374d29fc77e370014dce4fd6a55b58ad608de8074b0be5571701724da31",
nft_bridge: undefined,
nft_bridge:
"0x46da3d4c569388af61f951bdd1153f4c875f90c2991f6b2d0a38e2161a40852c",
},
sui: {
core: undefined,