solana: fix token metadata program interaction (#2913)

* testing: fix pubkey caused by 0b0b9cea70

* solana: fix token-metadata forked dependency

* sdk/js: fix tokenMetadata and Solana IDLs

* testing: fix tests; add token-metadata dependency

---------

Co-authored-by: A5 Pickle <a5-pickle@users.noreply.github.com>
This commit is contained in:
A5 Pickle 2023-05-15 15:55:03 -04:00 committed by GitHub
parent 3a28b6169a
commit 6f8c8430ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 833 additions and 903 deletions

View File

@ -177,7 +177,7 @@ jobs:
export PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}"
mkdir -p "${BPF_OUT_DIR}"
cp modules/token_bridge/token-metadata/spl_token_metadata.so "${BPF_OUT_DIR}"
cp external/mpl_token_metadata.so "${BPF_OUT_DIR}"
BPF_PACKAGES=(
bridge/program/Cargo.toml

View File

@ -52,7 +52,7 @@ spec:
- /opt/solana/deps/cpi_poster.so
- --bpf-program
- metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s
- /opt/solana/deps/spl_token_metadata.so
- /opt/solana/deps/mpl_token_metadata.so
- --bpf-program
- Ex9bCdVMSfx7EzB3pgSi2R4UHwJAXvTw18rBQm5YQ8gK
- /opt/solana/deps/wormhole_migration.so

View File

@ -1,3 +1,7 @@
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import {
PublicKey,
PublicKeyInitData,
@ -5,26 +9,22 @@ import {
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
} from "@solana/spl-token";
import { createReadOnlyNftBridgeProgramInterface } from "../program";
import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
import {
deriveEndpointKey,
deriveNftBridgeConfigKey,
deriveWrappedMintKey,
deriveWrappedMetaKey,
deriveMintAuthorityKey,
} from "../accounts";
import {
isBytes,
ParsedNftTransferVaa,
parseNftTransferVaa,
SignedVaa,
} from "../../../vaa";
import { SplTokenMetadataProgram } from "../../utils";
import { TOKEN_METADATA_PROGRAM_ID } from "../../utils";
import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
import {
deriveEndpointKey,
deriveMintAuthorityKey,
deriveNftBridgeConfigKey,
deriveWrappedMetaKey,
deriveWrappedMintKey,
} from "../accounts";
import { createReadOnlyNftBridgeProgramInterface } from "../program";
export function createCompleteTransferWrappedInstruction(
nftBridgeProgramId: PublicKeyInitData,
@ -110,7 +110,7 @@ export function getCompleteTransferWrappedAccounts(
rent: SYSVAR_RENT_PUBKEY,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
splMetadataProgram: SplTokenMetadataProgram.programId,
splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,
wormholeProgram: new PublicKey(wormholeProgramId),
};

View File

@ -1,3 +1,4 @@
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
PublicKey,
PublicKeyInitData,
@ -5,26 +6,22 @@ import {
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { createReadOnlyNftBridgeProgramInterface } from "../program";
import { derivePostedVaaKey } from "../../wormhole";
import {
deriveEndpointKey,
deriveNftBridgeConfigKey,
deriveWrappedMintKey,
deriveWrappedMetaKey,
deriveMintAuthorityKey,
} from "../accounts";
import {
isBytes,
ParsedNftTransferVaa,
parseNftTransferVaa,
SignedVaa,
} from "../../../vaa";
import { deriveTokenMetadataKey, TOKEN_METADATA_PROGRAM_ID } from "../../utils";
import { derivePostedVaaKey } from "../../wormhole";
import {
deriveSplTokenMetadataKey,
SplTokenMetadataProgram,
} from "../../utils";
deriveEndpointKey,
deriveMintAuthorityKey,
deriveNftBridgeConfigKey,
deriveWrappedMetaKey,
deriveWrappedMintKey,
} from "../accounts";
import { createReadOnlyNftBridgeProgramInterface } from "../program";
export function createCompleteWrappedMetaInstruction(
nftBridgeProgramId: PublicKeyInitData,
@ -92,12 +89,12 @@ export function getCompleteWrappedMetaAccounts(
),
mint,
wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint),
splMetadata: deriveSplTokenMetadataKey(mint),
splMetadata: deriveTokenMetadataKey(mint),
mintAuthority: deriveMintAuthorityKey(nftBridgeProgramId),
rent: SYSVAR_RENT_PUBKEY,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
splMetadataProgram: SplTokenMetadataProgram.programId,
splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
wormholeProgram: new PublicKey(wormholeProgramId),
};
}

View File

@ -1,21 +1,18 @@
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
PublicKey,
PublicKeyInitData,
TransactionInstruction,
} from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { createReadOnlyNftBridgeProgramInterface } from "../program";
import { TOKEN_METADATA_PROGRAM_ID, deriveTokenMetadataKey } from "../../utils";
import { getPostMessageAccounts } from "../../wormhole";
import {
deriveAuthoritySignerKey,
deriveCustodyKey,
deriveCustodySignerKey,
deriveNftBridgeConfigKey,
deriveCustodyKey,
} from "../accounts";
import {
deriveSplTokenMetadataKey,
SplTokenMetadataProgram,
} from "../../utils";
import { createReadOnlyNftBridgeProgramInterface } from "../program";
export function createTransferNativeInstruction(
nftBridgeProgramId: PublicKeyInitData,
@ -103,7 +100,7 @@ export function getTransferNativeAccounts(
config: deriveNftBridgeConfigKey(nftBridgeProgramId),
from: new PublicKey(from),
mint: new PublicKey(mint),
splMetadata: deriveSplTokenMetadataKey(mint),
splMetadata: deriveTokenMetadataKey(mint),
custody: deriveCustodyKey(nftBridgeProgramId, mint),
authoritySigner: deriveAuthoritySignerKey(nftBridgeProgramId),
custodySigner: deriveCustodySignerKey(nftBridgeProgramId),
@ -116,7 +113,7 @@ export function getTransferNativeAccounts(
rent,
systemProgram,
tokenProgram: TOKEN_PROGRAM_ID,
splMetadataProgram: SplTokenMetadataProgram.programId,
splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
wormholeProgram: new PublicKey(wormholeProgramId),
};
}

View File

@ -1,10 +1,10 @@
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
PublicKey,
PublicKeyInitData,
TransactionInstruction,
} from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { createReadOnlyNftBridgeProgramInterface } from "../program";
import { TOKEN_METADATA_PROGRAM_ID, deriveTokenMetadataKey } from "../../utils";
import { getPostMessageAccounts } from "../../wormhole";
import {
deriveAuthoritySignerKey,
@ -12,10 +12,7 @@ import {
deriveWrappedMetaKey,
deriveWrappedMintKey,
} from "../accounts";
import {
deriveSplTokenMetadataKey,
SplTokenMetadataProgram,
} from "../../utils";
import { createReadOnlyNftBridgeProgramInterface } from "../program";
export function createTransferWrappedInstruction(
nftBridgeProgramId: PublicKeyInitData,
@ -120,7 +117,7 @@ export function getTransferWrappedAccounts(
fromOwner: new PublicKey(fromOwner),
mint,
wrappedMeta: deriveWrappedMetaKey(nftBridgeProgramId, mint),
splMetadata: deriveSplTokenMetadataKey(mint),
splMetadata: deriveTokenMetadataKey(mint),
authoritySigner: deriveAuthoritySignerKey(nftBridgeProgramId),
wormholeBridge,
wormholeMessage,
@ -131,7 +128,7 @@ export function getTransferWrappedAccounts(
rent,
systemProgram,
tokenProgram: TOKEN_PROGRAM_ID,
splMetadataProgram: SplTokenMetadataProgram.programId,
splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
wormholeProgram: new PublicKey(wormholeProgramId),
};
}

View File

@ -11,7 +11,7 @@ import {
} from "../../../utils";
import { deriveAddress, getAccountData } from "../../utils";
export { deriveSplTokenMetadataKey } from "../../utils/splMetadata";
export { deriveTokenMetadataKey } from "../../utils/tokenMetadata";
export function deriveWrappedMintKey(
tokenBridgeProgramId: PublicKeyInitData,

View File

@ -6,7 +6,7 @@ import {
import { createReadOnlyTokenBridgeProgramInterface } from "../program";
import { getPostMessageAccounts } from "../../wormhole";
import {
deriveSplTokenMetadataKey,
deriveTokenMetadataKey,
deriveTokenBridgeConfigKey,
deriveWrappedMetaKey,
} from "../accounts";
@ -83,7 +83,7 @@ export function getAttestTokenAccounts(
config: deriveTokenBridgeConfigKey(tokenBridgeProgramId),
mint: new PublicKey(mint),
wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
splMetadata: deriveSplTokenMetadataKey(mint),
splMetadata: deriveTokenMetadataKey(mint),
wormholeBridge,
wormholeMessage: new PublicKey(message),
wormholeEmitter,

View File

@ -1,3 +1,4 @@
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import {
PublicKey,
PublicKeyInitData,
@ -5,24 +6,23 @@ import {
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
import { createReadOnlyTokenBridgeProgramInterface } from "../program";
import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
import {
deriveEndpointKey,
deriveMintAuthorityKey,
deriveSplTokenMetadataKey,
deriveWrappedMetaKey,
deriveTokenBridgeConfigKey,
deriveWrappedMintKey,
} from "../accounts";
import {
isBytes,
parseAttestMetaVaa,
ParsedAttestMetaVaa,
SignedVaa,
} from "../../../vaa";
import { SplTokenMetadataProgram } from "../../utils";
import { TOKEN_METADATA_PROGRAM_ID } from "../../utils";
import { deriveClaimKey, derivePostedVaaKey } from "../../wormhole";
import {
deriveEndpointKey,
deriveMintAuthorityKey,
deriveTokenBridgeConfigKey,
deriveTokenMetadataKey,
deriveWrappedMetaKey,
deriveWrappedMintKey,
} from "../accounts";
import { createReadOnlyTokenBridgeProgramInterface } from "../program";
export function createCreateWrappedInstruction(
tokenBridgeProgramId: PublicKeyInitData,
@ -96,12 +96,12 @@ export function getCreateWrappedAccounts(
),
mint,
wrappedMeta: deriveWrappedMetaKey(tokenBridgeProgramId, mint),
splMetadata: deriveSplTokenMetadataKey(mint),
splMetadata: deriveTokenMetadataKey(mint),
mintAuthority: deriveMintAuthorityKey(tokenBridgeProgramId),
rent: SYSVAR_RENT_PUBKEY,
systemProgram: SystemProgram.programId,
tokenProgram: TOKEN_PROGRAM_ID,
splMetadataProgram: SplTokenMetadataProgram.programId,
splMetadataProgram: TOKEN_METADATA_PROGRAM_ID,
wormholeProgram: new PublicKey(wormholeProgramId),
};
}

View File

@ -2,5 +2,5 @@ export * from "./account";
export * from "./bpfLoaderUpgradeable";
export * from "./connection";
export * from "./secp256k1";
export * from "./splMetadata";
export * from "./tokenMetadata";
export * from "./transaction";

View File

@ -1,331 +0,0 @@
import {
AccountMeta,
Commitment,
Connection,
PublicKey,
PublicKeyInitData,
SystemProgram,
SYSVAR_RENT_PUBKEY,
TransactionInstruction,
} from "@solana/web3.js";
import {
deriveAddress,
getAccountData,
newAccountMeta,
newReadOnlyAccountMeta,
} from "./account";
export class Creator {
address: PublicKey;
verified: boolean;
share: number;
constructor(address: PublicKeyInitData, verified: boolean, share: number) {
this.address = new PublicKey(address);
this.verified = verified;
this.share = share;
}
static size: number = 34;
serialize() {
const serialized = Buffer.alloc(Creator.size);
serialized.write(this.address.toBuffer().toString("hex"), 0, "hex");
if (this.verified) {
serialized.writeUInt8(1, 32);
}
serialized.writeUInt8(this.share, 33);
return serialized;
}
static deserialize(data: Buffer): Creator {
const address = data.subarray(0, 32);
const verified = data.readUInt8(32) > 0;
const share = data.readUInt8(33);
return new Creator(address, verified, share);
}
}
export class Data {
name: string;
symbol: string;
uri: string;
sellerFeeBasisPoints: number;
creators: Creator[] | null;
constructor(
name: string,
symbol: string,
uri: string,
sellerFeeBasisPoints: number,
creators: Creator[] | null
) {
this.name = name;
this.symbol = symbol;
this.uri = uri;
this.sellerFeeBasisPoints = sellerFeeBasisPoints;
this.creators = creators;
}
serialize() {
const nameLen = this.name.length;
const symbolLen = this.symbol.length;
const uriLen = this.uri.length;
const creators = this.creators;
const [creatorsLen, creatorsSize] = (() => {
if (creators === null) {
return [0, 0];
}
const creatorsLen = creators.length;
return [creatorsLen, 4 + creatorsLen * Creator.size];
})();
const serialized = Buffer.alloc(
15 + nameLen + symbolLen + uriLen + creatorsSize
);
serialized.writeUInt32LE(nameLen, 0);
serialized.write(this.name, 4);
serialized.writeUInt32LE(symbolLen, 4 + nameLen);
serialized.write(this.symbol, 8 + nameLen);
serialized.writeUInt32LE(uriLen, 8 + nameLen + symbolLen);
serialized.write(this.uri, 12 + nameLen + symbolLen);
serialized.writeUInt16LE(
this.sellerFeeBasisPoints,
12 + nameLen + symbolLen + uriLen
);
if (creators === null) {
serialized.writeUInt8(0, 14 + nameLen + symbolLen + uriLen);
} else {
serialized.writeUInt8(1, 14 + nameLen + symbolLen + uriLen);
serialized.writeUInt32LE(creatorsLen, 15 + nameLen + symbolLen + uriLen);
for (let i = 0; i < creatorsLen; ++i) {
const creator = creators.at(i)!;
const idx = 19 + nameLen + symbolLen + uriLen + i * Creator.size;
serialized.write(creator.serialize().toString("hex"), idx, "hex");
}
}
return serialized;
}
static deserialize(data: Buffer): Data {
const nameLen = data.readUInt32LE(0);
const name = data.subarray(4, 4 + nameLen).toString();
const symbolLen = data.readUInt32LE(4 + nameLen);
const symbol = data
.subarray(8 + nameLen, 8 + nameLen + symbolLen)
.toString();
const uriLen = data.readUInt32LE(8 + nameLen + symbolLen);
const uri = data
.subarray(12 + nameLen + symbolLen, 12 + nameLen + symbolLen + uriLen)
.toString();
const sellerFeeBasisPoints = data.readUInt16LE(
12 + nameLen + symbolLen + uriLen
);
const optionCreators = data.readUInt8(14 + nameLen + symbolLen + uriLen);
const creators = (() => {
if (optionCreators == 0) {
return null;
}
const creators: Creator[] = [];
const creatorsLen = data.readUInt32LE(15 + nameLen + symbolLen + uriLen);
for (let i = 0; i < creatorsLen; ++i) {
const idx = 19 + nameLen + symbolLen + uriLen + i * Creator.size;
creators.push(
Creator.deserialize(data.subarray(idx, idx + Creator.size))
);
}
return creators;
})();
return new Data(name, symbol, uri, sellerFeeBasisPoints, creators);
}
}
export class CreateMetadataAccountArgs extends Data {
isMutable: boolean;
constructor(
name: string,
symbol: string,
uri: string,
sellerFeeBasisPoints: number,
creators: Creator[] | null,
isMutable: boolean
) {
super(name, symbol, uri, sellerFeeBasisPoints, creators);
this.isMutable = isMutable;
}
static serialize(
name: string,
symbol: string,
uri: string,
sellerFeeBasisPoints: number,
creators: Creator[] | null,
isMutable: boolean
) {
return new CreateMetadataAccountArgs(
name,
symbol,
uri,
sellerFeeBasisPoints,
creators,
isMutable
).serialize();
}
static serializeInstructionData(
name: string,
symbol: string,
uri: string,
sellerFeeBasisPoints: number,
creators: Creator[] | null,
isMutable: boolean
) {
return Buffer.concat([
Buffer.alloc(1, 0),
CreateMetadataAccountArgs.serialize(
name,
symbol,
uri,
sellerFeeBasisPoints,
creators,
isMutable
),
]);
}
serialize() {
return Buffer.concat([
super.serialize(),
Buffer.alloc(1, this.isMutable ? 1 : 0),
]);
}
}
export class SplTokenMetadataProgram {
/**
* @internal
*/
constructor() {}
/**
* Public key that identifies the SPL Token Metadata program
*/
static programId: PublicKey = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
static createMetadataAccounts(
payer: PublicKey,
mint: PublicKey,
mintAuthority: PublicKey,
name: string,
symbol: string,
updateAuthority: PublicKey,
updateAuthorityIsSigner: boolean = false,
uri?: string,
creators?: Creator[] | null,
sellerFeeBasisPoints?: number,
isMutable: boolean = false,
metadataAccount: PublicKey = deriveSplTokenMetadataKey(mint)
): TransactionInstruction {
const keys: AccountMeta[] = [
newAccountMeta(metadataAccount, false),
newReadOnlyAccountMeta(mint, false),
newReadOnlyAccountMeta(mintAuthority, true),
newReadOnlyAccountMeta(payer, true),
newReadOnlyAccountMeta(updateAuthority, updateAuthorityIsSigner),
newReadOnlyAccountMeta(SystemProgram.programId, false),
newReadOnlyAccountMeta(SYSVAR_RENT_PUBKEY, false),
];
const data = CreateMetadataAccountArgs.serializeInstructionData(
name,
symbol,
uri === undefined ? "" : uri,
sellerFeeBasisPoints === undefined ? 0 : sellerFeeBasisPoints,
creators === undefined ? null : creators,
isMutable
);
return {
programId: SplTokenMetadataProgram.programId,
keys,
data,
};
}
}
export function deriveSplTokenMetadataKey(mint: PublicKeyInitData): PublicKey {
return deriveAddress(
[
Buffer.from("metadata"),
SplTokenMetadataProgram.programId.toBuffer(),
new PublicKey(mint).toBuffer(),
],
SplTokenMetadataProgram.programId
);
}
export enum Key {
Uninitialized,
EditionV1,
MasterEditionV1,
ReservationListV1,
MetadataV1,
ReservationListV2,
MasterEditionV2,
EditionMarker,
}
export class Metadata {
key: Key;
updateAuthority: PublicKey;
mint: PublicKey;
data: Data;
primarySaleHappened: boolean;
isMutable: boolean;
constructor(
key: number,
updateAuthority: PublicKeyInitData,
mint: PublicKeyInitData,
data: Data,
primarySaleHappened: boolean,
isMutable: boolean
) {
this.key = key as Key;
this.updateAuthority = new PublicKey(updateAuthority);
this.mint = new PublicKey(mint);
this.data = data;
this.primarySaleHappened = primarySaleHappened;
this.isMutable = isMutable;
}
static deserialize(data: Buffer): Metadata {
const key = data.readUInt8(0);
const updateAuthority = data.subarray(1, 33);
const mint = data.subarray(33, 65);
const meta = Data.deserialize(data.subarray(65));
const metaLen = meta.serialize().length;
const primarySaleHappened = data.readUInt8(65 + metaLen) > 0;
const isMutable = data.readUInt8(66 + metaLen) > 0;
return new Metadata(
key,
updateAuthority,
mint,
meta,
primarySaleHappened,
isMutable
);
}
}
export async function getMetadata(
connection: Connection,
mint: PublicKeyInitData,
commitment?: Commitment
): Promise<Metadata> {
return connection
.getAccountInfo(deriveSplTokenMetadataKey(mint), commitment)
.then((info) => Metadata.deserialize(getAccountData(info)));
}

View File

@ -0,0 +1,17 @@
import { PublicKey, PublicKeyInitData } from "@solana/web3.js";
import { deriveAddress } from "./account";
export const TOKEN_METADATA_PROGRAM_ID = new PublicKey(
"metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"
);
export function deriveTokenMetadataKey(mint: PublicKeyInitData): PublicKey {
return deriveAddress(
[
Buffer.from("metadata"),
TOKEN_METADATA_PROGRAM_ID.toBuffer(),
new PublicKey(mint).toBuffer(),
],
TOKEN_METADATA_PROGRAM_ID
);
}

28
solana/Cargo.lock generated
View File

@ -1847,6 +1847,19 @@ dependencies = [
"syn 1.0.91",
]
[[package]]
name = "mpl-token-metadata"
version = "0.0.1"
source = "git+https://github.com/wormhole-foundation/metaplex-program-library?rev=a7ab32ab0defd89c98f205c80ebdaf77ed60152d#a7ab32ab0defd89c98f205c80ebdaf77ed60152d"
dependencies = [
"borsh",
"num-derive",
"num-traits",
"solana-program",
"spl-token",
"thiserror",
]
[[package]]
name = "nft-bridge"
version = "0.1.0"
@ -1857,6 +1870,7 @@ dependencies = [
"hex",
"hex-literal",
"libsecp256k1",
"mpl-token-metadata",
"primitive-types",
"rand 0.7.3",
"rocksalt",
@ -1868,7 +1882,6 @@ dependencies = [
"solitaire",
"spl-associated-token-account",
"spl-token",
"spl-token-metadata",
"wasm-bindgen",
"wormhole-bridge-solana",
]
@ -3985,15 +3998,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "spl-token-metadata"
version = "0.0.1"
dependencies = [
"borsh",
"solana-program",
"spl-token",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@ -4269,6 +4273,7 @@ dependencies = [
"hex",
"hex-literal",
"libsecp256k1",
"mpl-token-metadata",
"primitive-types",
"rand 0.7.3",
"rocksalt",
@ -4279,7 +4284,6 @@ dependencies = [
"solana-sdk",
"solitaire",
"spl-token",
"spl-token-metadata",
"wasm-bindgen",
"wormhole-bridge-solana",
]
@ -4292,6 +4296,7 @@ dependencies = [
"borsh",
"clap",
"hex",
"mpl-token-metadata",
"rand 0.7.3",
"shellexpand",
"solana-clap-utils",
@ -4300,7 +4305,6 @@ dependencies = [
"solana-program",
"solana-sdk",
"solitaire",
"spl-token-metadata",
"token-bridge",
]

View File

@ -15,6 +15,7 @@ COPY migration migration
COPY Cargo.toml Cargo.toml
COPY Cargo.lock Cargo.lock
COPY solitaire solitaire
COPY external external
ENV RUST_LOG="solana_runtime::system_instruction_processor=trace,solana_runtime::message_processor=trace,solana_bpf_loader=debug,solana_rbpf=debug"
ENV RUST_BACKTRACE=1
@ -40,7 +41,7 @@ RUN --mount=type=cache,target=target,id=build \
cp target/deploy/wormhole_migration.so /opt/solana/deps/wormhole_migration.so && \
cp target/deploy/token_bridge.so /opt/solana/deps/token_bridge.so && \
cp target/deploy/nft_bridge.so /opt/solana/deps/nft_bridge.so && \
cp modules/token_bridge/token-metadata/spl_token_metadata.so /opt/solana/deps/spl_token_metadata.so
cp external/mpl_token_metadata.so /opt/solana/deps/mpl_token_metadata.so
FROM scratch AS export-stage
COPY --from=builder /opt/solana/deps /

BIN
solana/external/mpl_token_metadata.so vendored Executable file

Binary file not shown.

View File

@ -29,7 +29,7 @@ solana-program = "*"
spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }
spl-associated-token-account = { version = "1.0.2", features = ["no-entrypoint"] }
primitive-types = { version = "0.9.0", default-features = false }
spl-token-metadata = { path = "../../token_bridge/token-metadata" }
spl-token-metadata = { git = "https://github.com/wormhole-foundation/metaplex-program-library", rev = "a7ab32ab0defd89c98f205c80ebdaf77ed60152d", package = "mpl-token-metadata" }
wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true }
serde = { version = "1.0", features = ["derive"] }
@ -40,5 +40,3 @@ libsecp256k1 = { version = "0.6.0", features = [] }
rand = "0.7.3"
solana-program-test = "=1.10.31"
solana-sdk = "=1.10.31"
spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }
spl-token-metadata = { path = "../../token_bridge/token-metadata" }

View File

@ -1,4 +1,7 @@
use crate::types::*;
use crate::{
types::*,
TokenBridgeError,
};
use bridge::{
accounts::BridgeData,
api::ForeignAddress,
@ -9,6 +12,7 @@ use solitaire::{
processors::seeded::Seeded,
*,
};
use spl_token_metadata::state::Key::MetadataV1;
pub type AuthoritySigner<'b> = Derive<Info<'b>, "authority_signer">;
pub type CustodySigner<'b> = Derive<Info<'b>, "custody_signer">;
@ -106,3 +110,44 @@ impl<'b> Seeded<&SplTokenMetaDerivationData> for SplTokenMeta<'b> {
]
}
}
/// This method removes code duplication when checking token metadata. When metadata is read for
/// attestation and transfers, Token Bridge does not invoke Metaplex's Token Metadata program, so
/// it must validate the account the same way Token Metadata program does to ensure the correct
/// account is passed into Token Bridge's instruction context.
pub fn deserialize_and_verify_metadata(
info: &Info,
derivation_data: SplTokenMetaDerivationData,
) -> Result<spl_token_metadata::state::Metadata> {
// Verify pda.
info.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
// There must be account data for token's metadata.
if info.data_is_empty() {
return Err(TokenBridgeError::NonexistentTokenMetadataAccount.into());
}
// Account must belong to Metaplex Token Metadata program.
if *info.owner != spl_token_metadata::id() {
return Err(TokenBridgeError::WrongAccountOwner.into());
}
// Account must be the expected Metadata length.
if info.data_len() != spl_token_metadata::state::MAX_METADATA_LEN {
return Err(TokenBridgeError::InvalidMetadata.into());
}
let mut data: &[u8] = &info.data.borrow_mut();
// Unfortunately we cannot use `map_err` easily, so we will match certain deserialization conditions.
match spl_token_metadata::utils::meta_deser_unchecked(&mut data) {
Ok(deserialized) => {
if deserialized.key == MetadataV1 {
Ok(deserialized)
} else {
Err(TokenBridgeError::NotMetadataV1Account.into())
}
}
_ => Err(TokenBridgeError::InvalidMetadata.into()),
}
}

View File

@ -382,7 +382,7 @@ pub fn complete_wrapped_meta(
symbol.retain(|&c| c != '\u{FFFD}');
let symbol: String = symbol.iter().collect();
let spl_token_metadata_ix = spl_token_metadata::instruction::create_metadata_accounts(
let spl_token_metadata_ix = spl_token_metadata::instruction::create_metadata_accounts_v3(
spl_token_metadata::id(),
*accs.spl_metadata.key,
*accs.mint.info().key,
@ -396,6 +396,9 @@ pub fn complete_wrapped_meta(
0,
false,
true,
None,
None,
None,
);
invoke_seeded(&spl_token_metadata_ix, ctx, &accs.mint_authority, None)?;

View File

@ -1,5 +1,6 @@
use crate::{
accounts::{
deserialize_and_verify_metadata,
AuthoritySigner,
ConfigAccount,
CoreBridge,
@ -17,11 +18,7 @@ use crate::{
messages::PayloadTransfer,
types::*,
TokenBridgeError,
TokenBridgeError::{
InvalidMetadata,
TokenNotNFT,
WrongAccountOwner,
},
TokenBridgeError::WrongAccountOwner,
};
use bridge::{
api::PostMessageData,
@ -50,7 +47,6 @@ use solitaire::{
CreationLamports::Exempt,
*,
};
use spl_token_metadata::state::Metadata;
#[derive(FromAccounts)]
pub struct TransferNative<'b> {
@ -123,24 +119,11 @@ pub fn transfer_native(
accs.custody
.verify_derivation(ctx.program_id, &derivation_data)?;
let derivation_data: SplTokenMetaDerivationData = (&*accs).into();
accs.spl_metadata
.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
// Verify mints
if accs.from.mint != *accs.mint.info().key {
return Err(TokenBridgeError::InvalidMint.into());
}
// Token must have metadata
if accs.spl_metadata.data_is_empty() {
return Err(TokenNotNFT.into());
}
if *accs.spl_metadata.owner != spl_token_metadata::id() {
return Err(WrongAccountOwner.into());
}
// Verify that the token is not a wrapped token
if let COption::Some(mint_authority) = accs.mint.mint_authority {
if mint_authority == MintSigner::key(None, ctx.program_id) {
@ -180,8 +163,7 @@ pub fn transfer_native(
);
invoke(&transfer_ix, ctx.accounts)?;
let metadata: Metadata =
Metadata::from_account_info(accs.spl_metadata.info()).ok_or(InvalidMetadata)?;
let metadata = deserialize_and_verify_metadata(&accs.spl_metadata, (&*accs).into())?;
// Post message
// Given there is no tokenID equivalent on Solana and each distinct token address is translated
@ -326,21 +308,7 @@ pub fn transfer_wrapped(
accs.wrapped_meta
.verify_derivation(ctx.program_id, &derivation_data)?;
// Token must have metadata
if accs.spl_metadata.data_is_empty() {
return Err(TokenNotNFT.into());
}
let derivation_data: SplTokenMetaDerivationData = (&*accs).into();
accs.spl_metadata
.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
if *accs.spl_metadata.owner != spl_token_metadata::id() {
return Err(WrongAccountOwner.into());
}
let metadata: Metadata =
Metadata::from_account_info(accs.spl_metadata.info()).ok_or(InvalidMetadata)?;
let metadata = deserialize_and_verify_metadata(&accs.spl_metadata, (&*accs).into())?;
// Post message
let payload = PayloadTransfer {

View File

@ -59,9 +59,10 @@ pub enum TokenBridgeError {
TokenNotNative,
UninitializedMint,
WrongAccountOwner,
TokenNotNFT,
NonexistentTokenMetadataAccount,
InvalidAssociatedAccount,
InvalidRecipient,
NotMetadataV1Account,
}
impl From<TokenBridgeError> for SolitaireError {

View File

@ -128,7 +128,7 @@ mod helpers {
);
let mut builder = ProgramTest::new("bridge", program, processor!(bridge::solitaire));
builder.add_program("spl_token_metadata", spl_token_metadata::id(), None);
builder.add_program("mpl_token_metadata", spl_token_metadata::id(), None);
builder.add_program(
"nft_bridge",
token_program,
@ -506,7 +506,8 @@ mod helpers {
client,
payer,
&[payer, mint_authority],
&[spl_token_metadata::instruction::create_metadata_accounts(
&[
spl_token_metadata::instruction::create_metadata_accounts_v3(
spl_token_metadata::id(),
metadata_account,
mint,
@ -520,7 +521,11 @@ mod helpers {
0,
false,
false,
)],
None,
None,
None,
),
],
CommitmentLevel::Processed,
)
.await

View File

@ -17,4 +17,4 @@ solana-cli-config = "=1.10.31"
solitaire = { path = "../../../solitaire/program" }
solana-clap-utils = "=1.10.31"
hex = "0.4.3"
spl-token-metadata = { path = "../token-metadata" }
spl-token-metadata = { git = "https://github.com/wormhole-foundation/metaplex-program-library", rev = "a7ab32ab0defd89c98f205c80ebdaf77ed60152d", package = "mpl-token-metadata" }

View File

@ -107,7 +107,7 @@ fn command_create_meta(
)
.0;
println!("Meta account: {}", meta_acc);
let ix = spl_token_metadata::instruction::create_metadata_accounts(
let ix = spl_token_metadata::instruction::create_metadata_accounts_v3(
spl_token_metadata::id(),
meta_acc,
*mint,
@ -121,6 +121,9 @@ fn command_create_meta(
0,
false,
false,
None,
None,
None,
);
let mut transaction = Transaction::new_with_payer(&[ix], Some(&config.fee_payer.pubkey()));
@ -334,9 +337,15 @@ fn main() {
&spl_token_metadata::id(),
)
.0;
let meta_info = config.rpc_client.get_account(&meta_acc).unwrap();
let meta_info =
spl_token_metadata::state::Metadata::from_bytes(&meta_info.data).unwrap();
let meta_info = spl_token_metadata::utils::meta_deser_unchecked(
&mut config
.rpc_client
.get_account(&meta_acc)
.unwrap()
.data
.as_slice(),
)
.unwrap();
println!("Key: {:?}", meta_info.key);
println!("Mint: {}", meta_info.mint);
println!("Metadata Key: {}", meta_acc);

View File

@ -28,7 +28,7 @@ sha3 = "0.9.1"
solana-program = "*"
spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }
primitive-types = { version = "0.9.0", default-features = false }
spl-token-metadata = { path = "../token-metadata" }
spl-token-metadata = { git = "https://github.com/wormhole-foundation/metaplex-program-library", rev = "a7ab32ab0defd89c98f205c80ebdaf77ed60152d", package = "mpl-token-metadata" }
wasm-bindgen = { version = "0.2.74", features = ["serde-serialize"], optional = true }
serde = { version = "1.0", features = ["derive"] }
@ -39,5 +39,3 @@ libsecp256k1 = { version = "0.6.0", features = [] }
rand = "0.7.3"
solana-program-test = "=1.10.31"
solana-sdk = "=1.10.31"
spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }
spl-token-metadata = { path = "../token-metadata" }

View File

@ -1,4 +1,7 @@
use crate::types::*;
use crate::{
types::*,
TokenBridgeError,
};
use bridge::{
accounts::BridgeData,
api::ForeignAddress,
@ -8,6 +11,7 @@ use solitaire::{
processors::seeded::Seeded,
*,
};
use spl_token_metadata::state::Key::MetadataV1;
pub type AuthoritySigner<'b> = Derive<Info<'b>, "authority_signer">;
pub type CustodySigner<'b> = Derive<Info<'b>, "custody_signer">;
@ -101,3 +105,44 @@ impl<'b> Seeded<&SplTokenMetaDerivationData> for SplTokenMeta<'b> {
]
}
}
/// This method removes code duplication when checking token metadata. When metadata is read for
/// attestation and transfers, Token Bridge does not invoke Metaplex's Token Metadata program, so
/// it must validate the account the same way Token Metadata program does to ensure the correct
/// account is passed into Token Bridge's instruction context.
pub fn deserialize_and_verify_metadata(
info: &Info,
derivation_data: SplTokenMetaDerivationData,
) -> Result<spl_token_metadata::state::Metadata> {
// Verify pda.
info.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
// There must be account data for token's metadata.
if info.data_is_empty() {
return Err(TokenBridgeError::NonexistentTokenMetadataAccount.into());
}
// Account must belong to Metaplex Token Metadata program.
if *info.owner != spl_token_metadata::id() {
return Err(TokenBridgeError::WrongAccountOwner.into());
}
// Account must be the expected Metadata length.
if info.data_len() != spl_token_metadata::state::MAX_METADATA_LEN {
return Err(TokenBridgeError::InvalidMetadata.into());
}
let mut data: &[u8] = &info.data.borrow_mut();
// Unfortunately we cannot use `map_err` easily, so we will match certain deserialization conditions.
match spl_token_metadata::utils::meta_deser_unchecked(&mut data) {
Ok(deserialized) => {
if deserialized.key == MetadataV1 {
Ok(deserialized)
} else {
Err(TokenBridgeError::NotMetadataV1Account.into())
}
}
_ => Err(TokenBridgeError::InvalidMetadata.into()),
}
}

View File

@ -1,5 +1,6 @@
use crate::{
accounts::{
deserialize_and_verify_metadata,
ConfigAccount,
CoreBridge,
EmitterAccount,
@ -10,7 +11,6 @@ use crate::{
},
messages::PayloadAssetMeta,
types::*,
TokenBridgeError::*,
};
use bridge::{
api::PostMessageData,
@ -34,7 +34,6 @@ use solitaire::{
},
*,
};
use spl_token_metadata::state::Metadata;
#[derive(FromAccounts)]
pub struct AttestToken<'b> {
@ -118,16 +117,7 @@ pub fn attest_token(
// Assign metadata if an SPL Metadata account exists for the SPL token in question.
if !accs.spl_metadata.data_is_empty() {
let derivation_data: SplTokenMetaDerivationData = (&*accs).into();
accs.spl_metadata
.verify_derivation(&spl_token_metadata::id(), &derivation_data)?;
if *accs.spl_metadata.owner != spl_token_metadata::id() {
return Err(WrongAccountOwner.into());
}
let metadata: Metadata =
Metadata::from_account_info(accs.spl_metadata.info()).ok_or(InvalidMetadata)?;
let metadata = deserialize_and_verify_metadata(&accs.spl_metadata, (&*accs).into())?;
payload.name = metadata.data.name.clone();
payload.symbol = metadata.data.symbol;
}

View File

@ -1,5 +1,6 @@
use crate::{
accounts::{
deserialize_and_verify_metadata,
ConfigAccount,
Endpoint,
EndpointDerivationData,
@ -14,7 +15,6 @@ use crate::{
messages::PayloadAssetMeta,
TokenBridgeError::{
InvalidChain,
InvalidMetadata,
InvalidVAA,
},
INVALID_VAAS,
@ -40,10 +40,6 @@ use solitaire::{
*,
};
use spl_token_metadata::state::{
Data as SplData,
Metadata,
};
use std::cmp::min;
#[derive(FromAccounts)]
@ -164,7 +160,7 @@ pub fn create_accounts(
let name = truncate_utf8(&accs.vaa.name, 32 - 11) + " (Wormhole)";
let symbol = truncate_utf8(&accs.vaa.symbol, 10);
let spl_token_metadata_ix = spl_token_metadata::instruction::create_metadata_accounts(
let spl_token_metadata_ix = spl_token_metadata::instruction::create_metadata_accounts_v3(
spl_token_metadata::id(),
*accs.spl_metadata.key,
*accs.mint.info().key,
@ -178,6 +174,9 @@ pub fn create_accounts(
0,
false,
true,
None,
None,
None,
);
invoke_seeded(&spl_token_metadata_ix, ctx, &accs.mint_authority, None)?;
@ -194,28 +193,34 @@ pub fn update_accounts(
accs: &mut CreateWrapped,
_data: CreateWrappedData,
) -> Result<()> {
accs.spl_metadata.verify_derivation(
&spl_token_metadata::id(),
&SplTokenMetaDerivationData {
// Checks in this method are redundant with what occurs in `update_metadata_accounts_v2`, but we want to make
// sure that the account we are deserializing is legitimate.
let metadata = deserialize_and_verify_metadata(
&accs.spl_metadata,
SplTokenMetaDerivationData {
mint: *accs.mint.info().key,
},
)?;
let mut metadata: SplData = Metadata::from_account_info(accs.spl_metadata.info())
.ok_or(InvalidMetadata)?
.data;
// Normalize token metadata.
metadata.name = truncate_utf8(&accs.vaa.name, 32 - 11) + " (Wormhole)";
metadata.symbol = truncate_utf8(&accs.vaa.symbol, 10);
// Normalize token metadata's name and symbol.
let new_data_v2 = spl_token_metadata::state::DataV2 {
name: truncate_utf8(&accs.vaa.name, 32 - 11) + " (Wormhole)",
symbol: truncate_utf8(&accs.vaa.symbol, 10),
uri: metadata.data.uri,
seller_fee_basis_points: metadata.data.seller_fee_basis_points,
creators: metadata.data.creators,
collection: metadata.collection,
uses: metadata.uses,
};
// Update SPL Metadata
let spl_token_metadata_ix = spl_token_metadata::instruction::update_metadata_accounts(
let spl_token_metadata_ix = spl_token_metadata::instruction::update_metadata_accounts_v2(
spl_token_metadata::id(),
*accs.spl_metadata.key,
*accs.mint_authority.info().key,
None,
Some(metadata),
Some(new_data_v2),
None,
None,
);
invoke_seeded(&spl_token_metadata_ix, ctx, &accs.mint_authority, None)?;

View File

@ -89,6 +89,8 @@ pub enum TokenBridgeError {
InvalidFee,
InvalidRecipient,
InvalidVAA,
NonexistentTokenMetadataAccount,
NotMetadataV1Account,
}
impl From<TokenBridgeError> for SolitaireError {

View File

@ -131,7 +131,7 @@ mod helpers {
);
let mut builder = ProgramTest::new("bridge", program, processor!(bridge::solitaire));
builder.add_program("spl_token_metadata", spl_token_metadata::id(), None);
builder.add_program("mpl_token_metadata", spl_token_metadata::id(), None);
builder.add_program(
"token_bridge",
token_program,
@ -618,7 +618,8 @@ mod helpers {
client,
payer,
&[payer, mint_authority],
&[spl_token_metadata::instruction::create_metadata_accounts(
&[
spl_token_metadata::instruction::create_metadata_accounts_v3(
spl_token_metadata::id(),
metadata_account,
mint.pubkey(),
@ -632,7 +633,11 @@ mod helpers {
0,
false,
false,
)],
None,
None,
None,
),
],
CommitmentLevel::Processed,
)
.await

View File

@ -1,20 +0,0 @@
[package]
name = "spl-token-metadata"
version = "0.0.1"
description = "Metaplex Metadata"
authors = ["Metaplex Maintainers <maintainers@metaplex.com>"]
repository = "https://github.com/metaplex-foundation/metaplex"
license = "Apache-2.0"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
[features]
no-entrypoint = []
test-bpf = []
[dependencies]
borsh = "=0.9.3"
solana-program = "=1.10.31"
spl-token = { version = "=3.3.0", features = ["no-entrypoint"] }

View File

@ -1,8 +0,0 @@
---
title: Token Metadata Program
---
Fork of the SPL Token Metadata program from the Metaplex repository, this is
temporary until there are versioned releases we can compile against. Currently
the upstream version depends on a version of solana with conflicting versions
of borsh.

View File

@ -1,2 +0,0 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -1,138 +0,0 @@
use crate::state::{
Creator,
Data,
EDITION,
EDITION_MARKER_BIT_SIZE,
PREFIX,
};
use borsh::{
BorshDeserialize,
BorshSerialize,
};
use solana_program::{
instruction::{
AccountMeta,
Instruction,
},
pubkey::Pubkey,
sysvar,
};
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
/// Args for update call
pub struct UpdateMetadataAccountArgs {
pub data: Option<Data>,
pub update_authority: Option<Pubkey>,
pub primary_sale_happened: Option<bool>,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
/// Args for create call
pub struct CreateMetadataAccountArgs {
/// Note that unique metadatas are disabled for now.
pub data: Data,
/// Whether you want your metadata to be updateable in the future.
pub is_mutable: bool,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct CreateMasterEditionArgs {
/// If set, means that no more than this number of editions can ever be minted. This is immutable.
pub max_supply: Option<u64>,
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct MintNewEditionFromMasterEditionViaTokenArgs {
pub edition: u64,
}
/// Instructions supported by the Metadata program.
#[derive(BorshSerialize, BorshDeserialize, Clone)]
pub enum MetadataInstruction {
/// Create Metadata object.
/// 0. `[writable]` Metadata key (pda of ['metadata', program id, mint id])
/// 1. `[]` Mint of token asset
/// 2. `[signer]` Mint authority
/// 3. `[signer]` payer
/// 4. `[]` update authority info
/// 5. `[]` System program
/// 6. `[]` Rent info
CreateMetadataAccount(CreateMetadataAccountArgs),
/// Update a Metadata
/// 0. `[writable]` Metadata account
/// 1. `[signer]` Update authority key
UpdateMetadataAccount(UpdateMetadataAccountArgs),
}
/// Creates an CreateMetadataAccounts instruction
#[allow(clippy::too_many_arguments)]
pub fn create_metadata_accounts(
program_id: Pubkey,
metadata_account: Pubkey,
mint: Pubkey,
mint_authority: Pubkey,
payer: Pubkey,
update_authority: Pubkey,
name: String,
symbol: String,
uri: String,
creators: Option<Vec<Creator>>,
seller_fee_basis_points: u16,
update_authority_is_signer: bool,
is_mutable: bool,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(metadata_account, false),
AccountMeta::new_readonly(mint, false),
AccountMeta::new_readonly(mint_authority, true),
AccountMeta::new(payer, true),
AccountMeta::new_readonly(update_authority, update_authority_is_signer),
AccountMeta::new_readonly(solana_program::system_program::id(), false),
AccountMeta::new_readonly(sysvar::rent::id(), false),
],
data: MetadataInstruction::CreateMetadataAccount(CreateMetadataAccountArgs {
data: Data {
name,
symbol,
uri,
seller_fee_basis_points,
creators,
},
is_mutable,
})
.try_to_vec()
.unwrap(),
}
}
/// update metadata account instruction
pub fn update_metadata_accounts(
program_id: Pubkey,
metadata_account: Pubkey,
update_authority: Pubkey,
new_update_authority: Option<Pubkey>,
data: Option<Data>,
primary_sale_happened: Option<bool>,
) -> Instruction {
Instruction {
program_id,
accounts: vec![
AccountMeta::new(metadata_account, false),
AccountMeta::new_readonly(update_authority, true),
],
data: MetadataInstruction::UpdateMetadataAccount(UpdateMetadataAccountArgs {
data,
update_authority: new_update_authority,
primary_sale_happened,
})
.try_to_vec()
.unwrap(),
}
}

View File

@ -1,7 +0,0 @@
// The solana_program::declare_id! macro generates spurious import statements.
#[allow(unused_imports)]
pub mod instruction;
pub mod state;
pub mod utils;
solana_program::declare_id!("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s");

View File

@ -1,124 +0,0 @@
use crate::utils::try_from_slice_checked;
use borsh::{
BorshDeserialize,
BorshSerialize,
};
use solana_program::{
account_info::AccountInfo,
pubkey::Pubkey,
};
/// prefix used for PDAs to avoid certain collision attacks (https://en.wikipedia.org/wiki/Collision_attack#Chosen-prefix_collision_attack)
pub const PREFIX: &str = "metadata";
/// Used in seeds to make Edition model pda address
pub const EDITION: &str = "edition";
pub const RESERVATION: &str = "reservation";
pub const MAX_NAME_LENGTH: usize = 32;
pub const MAX_SYMBOL_LENGTH: usize = 10;
pub const MAX_URI_LENGTH: usize = 200;
pub const MAX_METADATA_LEN: usize = 1
+ 32
+ 32
+ MAX_NAME_LENGTH
+ MAX_SYMBOL_LENGTH
+ MAX_URI_LENGTH
+ MAX_CREATOR_LIMIT * MAX_CREATOR_LEN
+ 2
+ 1
+ 1
+ 198;
pub const MAX_EDITION_LEN: usize = 1 + 32 + 8 + 200;
// Large buffer because the older master editions have two pubkeys in them,
// need to keep two versions same size because the conversion process actually changes the same account
// by rewriting it.
pub const MAX_MASTER_EDITION_LEN: usize = 1 + 9 + 8 + 264;
pub const MAX_CREATOR_LIMIT: usize = 5;
pub const MAX_CREATOR_LEN: usize = 32 + 1 + 1;
pub const MAX_RESERVATIONS: usize = 200;
// can hold up to 200 keys per reservation, note: the extra 8 is for number of elements in the vec
pub const MAX_RESERVATION_LIST_V1_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 34 + 100;
// can hold up to 200 keys per reservation, note: the extra 8 is for number of elements in the vec
pub const MAX_RESERVATION_LIST_SIZE: usize = 1 + 32 + 8 + 8 + MAX_RESERVATIONS * 48 + 8 + 8 + 84;
pub const MAX_EDITION_MARKER_SIZE: usize = 32;
pub const EDITION_MARKER_BIT_SIZE: u64 = 248;
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone, Copy)]
pub enum Key {
Uninitialized,
EditionV1,
MasterEditionV1,
ReservationListV1,
MetadataV1,
ReservationListV2,
MasterEditionV2,
EditionMarker,
}
impl Default for Key {
fn default() -> Self {
Key::Uninitialized
}
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, Default, PartialEq, Debug, Clone)]
pub struct Data {
/// The name of the asset
pub name: String,
/// The symbol for the asset
pub symbol: String,
/// URI pointing to JSON representing the asset
pub uri: String,
/// Royalty basis points that goes to creators in secondary sales (0-10000)
pub seller_fee_basis_points: u16,
/// Array of creators, optional
pub creators: Option<Vec<Creator>>,
}
#[repr(C)]
#[derive(Clone, BorshSerialize, BorshDeserialize, Debug, Default)]
pub struct Metadata {
pub key: Key,
pub update_authority: Pubkey,
pub mint: Pubkey,
pub data: Data,
// Immutable, once flipped, all sales of this metadata are considered secondary.
pub primary_sale_happened: bool,
// Whether or not the data struct is mutable, default is not
pub is_mutable: bool,
}
impl Metadata {
pub fn from_bytes(a: &[u8]) -> Option<Metadata> {
try_from_slice_checked(a, Key::MetadataV1, MAX_METADATA_LEN)
}
pub fn from_account_info(a: &AccountInfo) -> Option<Metadata> {
try_from_slice_checked(&a.data.borrow_mut(), Key::MetadataV1, MAX_METADATA_LEN)
}
}
#[repr(C)]
#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)]
pub struct Creator {
pub address: Pubkey,
pub verified: bool,
// In percentages, NOT basis points ;) Watch out!
pub share: u8,
}

View File

@ -1,16 +0,0 @@
use crate::state::Key;
use borsh::BorshDeserialize;
use solana_program::borsh::try_from_slice_unchecked;
pub fn try_from_slice_checked<T: BorshDeserialize>(
data: &[u8],
data_type: Key,
data_size: usize,
) -> Option<T> {
if (data[0] != data_type as u8 && data[0] != Key::Uninitialized as u8)
|| data.len() != data_size
{
return None;
}
try_from_slice_unchecked(data).ok()
}

View File

@ -1,3 +1,5 @@
.test
artifacts
artifacts*
node_modules
wormhole-main
validator.log

View File

@ -7,15 +7,33 @@
node_modules:
yarn
artifacts: node_modules
cd ../../solana && DOCKER_BUILDKIT=1 docker build -f Dockerfile --build-arg BRIDGE_ADDRESS=agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC -o ../testing/solana-test-validator/artifacts .
artifacts:
cd ../../solana && \
DOCKER_BUILDKIT=1 docker build \
-f Dockerfile \
--build-arg BRIDGE_ADDRESS=agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC \
-o ../testing/solana-test-validator/artifacts .
artifacts-main:
git clone \
--depth 1 \
--branch main \
--filter=blob:none \
https://github.com/wormhole-foundation/wormhole \
wormhole-main
cd wormhole-main/solana && \
DOCKER_BUILDKIT=1 docker build \
-f Dockerfile \
--build-arg BRIDGE_ADDRESS=agnnozV7x6ffAhi8xVhBd5dShfLnuUKKPEMX1tJ1nDC \
-o ../../artifacts-main .
rm -rf wormhole-main
.PHONY: test
test: artifacts
test: node_modules artifacts-main artifacts
@echo "Running integration tests"
yarn run sdk-tests
.PHONY: clean
clean:
rm -rf artifacts node_modules validator.log .test
rm -rf artifacts artifacts-main wormhole-main node_modules validator.log .test

View File

@ -6,6 +6,7 @@
"lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check"
},
"dependencies": {
"@metaplex-foundation/mpl-token-metadata": "^2.11.1",
"@project-serum/anchor": "0.25.0",
"@solana/spl-token": "^0.3.1",
"@solana/web3.js": "^1.53.0",

View File

@ -15,9 +15,10 @@ ACCOUNTS=$ROOT/sdk-tests/accounts
TEST=$ROOT/.test
solana-test-validator --reset \
--bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s $ARTIFACTS/spl_token_metadata.so \
--bpf-program metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s $ARTIFACTS/mpl_token_metadata.so \
--account-dir $ACCOUNTS \
--ledger $TEST > validator.log 2>&1 &
sleep 2
sleep 5
### write program logs
PROGRAM_LOGS=$TEST/program-logs

View File

@ -69,7 +69,7 @@ describe("Deploy and Upgrade Programs", () => {
describe("Wormhole (Core Bridge)", () => {
it("Deploy and Initialize", async () => {
const artifactPath = `${__dirname}/../artifacts/bridge.so`;
const artifactPath = `${__dirname}/../artifacts-main/bridge.so`;
const programIdPath = `${__dirname}/keys/${CORE_BRIDGE_ADDRESS}.json`;
const upgradeAuthority = deriveUpgradeAuthorityKey(CORE_BRIDGE_ADDRESS);
@ -176,7 +176,7 @@ describe("Deploy and Upgrade Programs", () => {
describe("Token Bridge", () => {
it("Deploy and Initialize", async () => {
const artifactPath = `${__dirname}/../artifacts/token_bridge.so`;
const artifactPath = `${__dirname}/../artifacts-main/token_bridge.so`;
const programIdPath = `${__dirname}/keys/${TOKEN_BRIDGE_ADDRESS}.json`;
const upgradeAuthority = deriveUpgradeAuthorityKey(TOKEN_BRIDGE_ADDRESS);
@ -270,7 +270,7 @@ describe("Deploy and Upgrade Programs", () => {
describe("NFT Bridge", () => {
it("Deploy and Initialize", async () => {
const artifactPath = `${__dirname}/../artifacts/nft_bridge.so`;
const artifactPath = `${__dirname}/../artifacts-main/nft_bridge.so`;
const programIdPath = `${__dirname}/keys/${NFT_BRIDGE_ADDRESS}.json`;
const upgradeAuthority = deriveUpgradeAuthorityKey(NFT_BRIDGE_ADDRESS);

View File

@ -1,5 +1,9 @@
import { expect } from "chai";
import * as web3 from "@solana/web3.js";
import {
Metadata,
PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
} from "@metaplex-foundation/mpl-token-metadata";
import {
createMint,
getAccount,
@ -30,6 +34,7 @@ import {
deriveEndpointKey,
deriveMintAuthorityKey,
deriveRedeemerAccountKey,
deriveTokenMetadataKey,
deriveWrappedMintKey,
getAttestTokenAccounts,
getCompleteTransferNativeAccounts,
@ -54,7 +59,6 @@ import {
getTransferWrappedWithPayloadCpiAccounts,
NodeWallet,
signSendAndConfirmTransaction,
SplTokenMetadataProgram,
} from "../../../sdk/js/src/solana";
import {
deriveWormholeEmitterKey,
@ -80,6 +84,9 @@ import {
GUARDIAN_SET_INDEX,
LOCALHOST,
WETH_ADDRESS,
DEADBEEF_ADDRESS,
DEADBEEF_METADATA_ADDRESS,
DEADBEEF_MINT_ADDRESS,
} from "./helpers/consts";
import { ethAddressToBuffer, now } from "./helpers/utils";
import {
@ -517,7 +524,7 @@ describe("Token Bridge", () => {
"4CgrjMnDneBjBBEyXtcikLTbAWpHAD1cwn8W1sSSCLru"
);
expect(accounts.vaa.toString()).to.equal(
"AaZqjqvg8QirKetuR799Smfw5gyAWobUooGsvxzr1aoX"
"4NDyWDtRvfEdi48a9JgYG28m919hrcdW8gNgRg3jwU99"
);
expect(accounts.claim.toString()).to.equal(
"4dyk94hhqektDX9wUBCL1ZkyQC1Xn3QaTSAdJeZzbTcJ"
@ -538,9 +545,8 @@ describe("Token Bridge", () => {
expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
.true;
expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
expect(
accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
).is.true;
expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
.true;
expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true;
});
@ -1552,6 +1558,278 @@ describe("Token Bridge", () => {
Buffer.compare(wrappedMeta.tokenAddress, expectedTokenAddress)
).to.equal(0);
expect(wrappedMeta.originalDecimals).to.equal(decimals);
// check metadata
const expectedName = `${name} (Wormhole)`.padEnd(32, "\0");
const metadata = await Metadata.fromAccountAddress(
connection,
deriveTokenMetadataKey(mint)
);
expect(metadata.data.symbol.toString()).equals(symbol.padEnd(10, "\0"));
expect(metadata.data.name.toString()).equals(expectedName);
localVariables.oldName = expectedName;
});
it("Update (Create) Wrapped with New Metadata", async () => {
const tokenAddress = WETH_ADDRESS;
const oldName: string = localVariables.oldName;
const mint = deriveWrappedMintKey(
TOKEN_BRIDGE_ADDRESS,
ethereumTokenBridge.chain,
tokenAddress
);
// check existing metadata
{
const metadata = await Metadata.fromAccountAddress(
connection,
deriveTokenMetadataKey(mint)
);
expect(metadata.data.name.toString()).equals(oldName);
}
const decimals = 18;
const symbol = "WETH";
const name = "Wrapped Ether";
const nonce = 420;
const message = ethereumTokenBridge.publishAttestMeta(
tokenAddress,
decimals,
symbol,
name,
nonce
);
const signedVaa = guardians.addSignatures(
message,
[0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18]
);
const txSignatures = await postVaa(
connection,
wallet.signTransaction,
CORE_BRIDGE_ADDRESS,
wallet.key(),
signedVaa
).then((results) => results.map((result) => result.signature));
const postTx = txSignatures.pop()!;
for (const verifyTx of txSignatures) {
// console.log(`verifySignatures: ${verifyTx}`);
}
// console.log(`postVaa: ${postTx}`);
const createWrappedIx = createCreateWrappedInstruction(
TOKEN_BRIDGE_ADDRESS,
CORE_BRIDGE_ADDRESS,
wallet.key(),
signedVaa
);
const createWrappedTx = await web3.sendAndConfirmTransaction(
connection,
new web3.Transaction().add(createWrappedIx),
[wallet.signer()]
);
// console.log(`createWrappedTx: ${createWrappedTx}`);
// verify data
const parsed = parseAttestMetaVaa(signedVaa);
const messageData = await getPostedVaa(
connection,
CORE_BRIDGE_ADDRESS,
parsed.hash
).then((posted) => posted.message);
expect(messageData.consistencyLevel).to.equal(
ethereumTokenBridge.consistencyLevel
);
const expectedEmitter = ethAddressToBuffer(
ETHEREUM_TOKEN_BRIDGE_ADDRESS
);
expect(
Buffer.compare(messageData.emitterAddress, expectedEmitter)
).to.equal(0);
expect(messageData.emitterChain).to.equal(ethereumTokenBridge.chain);
expect(messageData.nonce).to.equal(nonce);
expect(messageData.sequence).to.equal(3n);
expect(messageData.vaaTime).to.equal(0);
expect(messageData.vaaVersion).to.equal(1);
expect(Buffer.compare(parsed.payload, messageData.payload)).to.equal(0);
const assetMeta = parseAttestMetaPayload(messageData.payload);
expect(assetMeta.payloadType).to.equal(2);
const expectedTokenAddress = ethAddressToBuffer(tokenAddress);
expect(
Buffer.compare(assetMeta.tokenAddress, expectedTokenAddress)
).to.equal(0);
expect(assetMeta.tokenChain).to.equal(ethereumTokenBridge.chain);
expect(assetMeta.decimals).to.equal(decimals);
expect(assetMeta.symbol).to.equal(symbol);
expect(assetMeta.name).to.equal(name);
// check wrapped mint
const mintInfo = await getMint(connection, mint);
expect(mintInfo.decimals).to.equal(8);
expect(mintInfo.mintAuthority).is.not.null;
expect(
mintInfo.mintAuthority?.equals(
deriveMintAuthorityKey(TOKEN_BRIDGE_ADDRESS)
)
).is.true;
expect(mintInfo.supply).to.equal(0n);
// check wrapped meta
const wrappedMeta = await getWrappedMeta(
connection,
TOKEN_BRIDGE_ADDRESS,
mint
);
expect(wrappedMeta.chain).to.equal(ethereumTokenBridge.chain);
expect(
Buffer.compare(wrappedMeta.tokenAddress, expectedTokenAddress)
).to.equal(0);
expect(wrappedMeta.originalDecimals).to.equal(decimals);
// check metadata
const metadata = await Metadata.fromAccountAddress(
connection,
deriveTokenMetadataKey(mint)
);
expect(metadata.data.name.toString()).not.equals(oldName);
expect(metadata.data.symbol.toString()).equals(symbol.padEnd(10, "\0"));
expect(metadata.data.name.toString()).equals(
`${name} (Wormhole)`.padEnd(32, "\0")
);
});
it("Update (Create) Wrapped with New Metadata for V1 Metadata Account", async () => {
const tokenAddress = DEADBEEF_ADDRESS;
const oldExpectedName = "Dead Beef (Wormhole)".padEnd(32, "\0");
// fetch previously created metadata account
// check wrapped mint
{
const mint = deriveWrappedMintKey(
TOKEN_BRIDGE_ADDRESS,
ethereumTokenBridge.chain,
tokenAddress
);
expect(mint.toString()).equals(DEADBEEF_MINT_ADDRESS);
const metadataKey = deriveTokenMetadataKey(mint);
expect(metadataKey.toString()).equals(DEADBEEF_METADATA_ADDRESS);
const metadata = await Metadata.fromAccountAddress(
connection,
metadataKey
);
expect(metadata.data.name.toString()).equals(oldExpectedName);
}
const decimals = 18;
const symbol = "BEEF";
const name = "Dead Beef Modified";
const nonce = 420;
const message = ethereumTokenBridge.publishAttestMeta(
tokenAddress,
decimals,
symbol,
name,
nonce
);
const signedVaa = guardians.addSignatures(
message,
[0, 1, 2, 3, 5, 7, 8, 9, 10, 12, 15, 16, 18]
);
const txSignatures = await postVaa(
connection,
wallet.signTransaction,
CORE_BRIDGE_ADDRESS,
wallet.key(),
signedVaa
).then((results) => results.map((result) => result.signature));
const postTx = txSignatures.pop()!;
for (const verifyTx of txSignatures) {
// console.log(`verifySignatures: ${verifyTx}`);
}
// console.log(`postVaa: ${postTx}`);
const createWrappedIx = createCreateWrappedInstruction(
TOKEN_BRIDGE_ADDRESS,
CORE_BRIDGE_ADDRESS,
wallet.key(),
signedVaa
);
const createWrappedTx = await web3.sendAndConfirmTransaction(
connection,
new web3.Transaction().add(createWrappedIx),
[wallet.signer()]
);
// console.log(`createWrappedTx: ${createWrappedTx}`);
// verify data
const parsed = parseAttestMetaVaa(signedVaa);
const messageData = await getPostedVaa(
connection,
CORE_BRIDGE_ADDRESS,
parsed.hash
).then((posted) => posted.message);
expect(messageData.consistencyLevel).to.equal(
ethereumTokenBridge.consistencyLevel
);
const expectedEmitter = ethAddressToBuffer(
ETHEREUM_TOKEN_BRIDGE_ADDRESS
);
expect(
Buffer.compare(messageData.emitterAddress, expectedEmitter)
).to.equal(0);
expect(messageData.emitterChain).to.equal(ethereumTokenBridge.chain);
expect(messageData.nonce).to.equal(nonce);
expect(messageData.sequence).to.equal(4n);
expect(messageData.vaaTime).to.equal(0);
expect(messageData.vaaVersion).to.equal(1);
expect(Buffer.compare(parsed.payload, messageData.payload)).to.equal(0);
const assetMeta = parseAttestMetaPayload(messageData.payload);
expect(assetMeta.payloadType).to.equal(2);
const expectedTokenAddress = ethAddressToBuffer(tokenAddress);
expect(
Buffer.compare(assetMeta.tokenAddress, expectedTokenAddress)
).to.equal(0);
expect(assetMeta.tokenChain).to.equal(ethereumTokenBridge.chain);
expect(assetMeta.decimals).to.equal(decimals);
expect(assetMeta.symbol).to.equal(symbol);
expect(assetMeta.name).to.equal(name);
// check wrapped mint
const mint = deriveWrappedMintKey(
TOKEN_BRIDGE_ADDRESS,
assetMeta.tokenChain,
assetMeta.tokenAddress
);
const mintInfo = await getMint(connection, mint);
expect(mintInfo.decimals).to.equal(8);
expect(mintInfo.mintAuthority).is.not.null;
expect(
mintInfo.mintAuthority?.equals(
deriveMintAuthorityKey(TOKEN_BRIDGE_ADDRESS)
)
).is.true;
expect(mintInfo.supply).to.equal(0n);
// check metadata
const metadata = await Metadata.fromAccountAddress(
connection,
deriveTokenMetadataKey(mint)
);
expect(metadata.data.name.toString()).not.equals(oldExpectedName);
expect(metadata.data.name.toString()).equals(
`${name} (Wormhole)`.padEnd(32, "\0")
);
});
it("Receive Token", async () => {
@ -1653,7 +1931,7 @@ describe("Token Bridge", () => {
).to.equal(0);
expect(messageData.emitterChain).to.equal(ethereumTokenBridge.chain);
expect(messageData.nonce).to.equal(nonce);
expect(messageData.sequence).to.equal(3n);
expect(messageData.sequence).to.equal(5n);
expect(messageData.vaaTime).to.equal(0);
expect(messageData.vaaVersion).to.equal(1);
expect(
@ -1900,7 +2178,7 @@ describe("Token Bridge", () => {
// nft bridge on Ethereum
const ethereumTokenBridge = new MockEthereumTokenBridge(
ETHEREUM_TOKEN_BRIDGE_ADDRESS,
3 // startSequence
10 // startSequence
);
describe("getOriginalAssetSolana", () => {

View File

@ -1,5 +1,10 @@
import { expect } from "chai";
import * as web3 from "@solana/web3.js";
import {
Metadata,
PROGRAM_ID as TOKEN_METADATA_PROGRAM_ID,
createCreateMetadataAccountV3Instruction,
} from "@metaplex-foundation/mpl-token-metadata";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
createMint,
@ -19,9 +24,8 @@ import {
import { postVaa } from "../../../sdk/js/src/solana/sendAndConfirmPostVaa";
import {
BpfLoaderUpgradeable,
getMetadata,
NodeWallet,
SplTokenMetadataProgram,
deriveTokenMetadataKey,
} from "../../../sdk/js/src/solana";
import {
deriveWormholeEmitterKey,
@ -115,26 +119,37 @@ describe("NFT Bridge", () => {
uri: "https://spl.solana.com/token#example-create-a-non-fungible-token",
};
const mint = localVariables.mint;
const name = localVariables.nftMeta.name;
const symbol = localVariables.nftMeta.symbol;
const updateAuthorityIsSigner = false;
const uri = localVariables.nftMeta.uri;
const creators = null;
const sellerFeeBasisPoints = 0;
const isMutable = false;
const createMetadataIx = SplTokenMetadataProgram.createMetadataAccounts(
wallet.key(),
const mint: web3.PublicKey = localVariables.mint;
const name: string = localVariables.nftMeta.name;
const symbol: string = localVariables.nftMeta.symbol;
const uri: string = localVariables.nftMeta.uri;
const accounts = {
metadata: deriveTokenMetadataKey(mint),
mint,
wallet.key(),
mintAuthority: wallet.key(),
payer: wallet.key(),
updateAuthority: wallet.key(),
};
const args = {
createMetadataAccountArgsV3: {
data: {
name,
symbol,
wallet.key(),
updateAuthorityIsSigner,
uri,
creators,
sellerFeeBasisPoints,
isMutable
sellerFeeBasisPoints: 0,
creators: null,
collection: null,
uses: null,
},
isMutable: false,
collectionDetails: null,
},
};
const createMetadataIx = createCreateMetadataAccountV3Instruction(
accounts,
args,
TOKEN_METADATA_PROGRAM_ID
);
const createMetadataTx = await web3.sendAndConfirmTransaction(
@ -323,9 +338,8 @@ describe("NFT Bridge", () => {
expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
.true;
expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
expect(
accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
).is.true;
expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
.true;
expect(
accounts.associatedTokenProgram.equals(ASSOCIATED_TOKEN_PROGRAM_ID)
).is.true;
@ -401,9 +415,8 @@ describe("NFT Bridge", () => {
expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
.true;
expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
expect(
accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
).is.true;
expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
.true;
expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true;
});
@ -467,9 +480,8 @@ describe("NFT Bridge", () => {
expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
.true;
expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
expect(
accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
).is.true;
expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
.true;
expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true;
});
@ -524,9 +536,8 @@ describe("NFT Bridge", () => {
expect(accounts.systemProgram.equals(web3.SystemProgram.programId)).to.be
.true;
expect(accounts.tokenProgram.equals(TOKEN_PROGRAM_ID)).is.true;
expect(
accounts.splMetadataProgram.equals(SplTokenMetadataProgram.programId)
).is.true;
expect(accounts.splMetadataProgram.equals(TOKEN_METADATA_PROGRAM_ID)).is
.true;
expect(accounts.wormholeProgram.equals(CORE_BRIDGE_ADDRESS)).is.true;
});
@ -792,8 +803,9 @@ describe("NFT Bridge", () => {
custodyAccount
).then((account) => account.amount);
const metadata = await getMetadata(connection, mint).then(
(info) => info.data
const metadata = await Metadata.fromAccountAddress(
connection,
deriveTokenMetadataKey(mint)
);
const tokenChain = 1;
@ -803,10 +815,10 @@ describe("NFT Bridge", () => {
const message = ethereumNftBridge.publishTransferNft(
NFT_TRANSFER_NATIVE_TOKEN_ADDRESS.toString("hex"),
tokenChain,
metadata.name,
metadata.symbol,
metadata.data.name,
metadata.data.symbol,
tokenId,
metadata.uri,
metadata.data.uri,
recipientChain,
mintAta.toBuffer().toString("hex"),
nonce

View File

@ -0,0 +1,13 @@
{
"pubkey": "A2mcN1tknMBAgMnCUF6ZT8tEJj1QE1AuCsquvXHgpWVJ",
"account": {
"lamports": 5616720,
"data": [
"BP0KziEuVYQFCsdn5PTCokaUsJwqy3LxSbhyCV219Z2d8nzo8j6OCMEC0yd3q42xDE00sIuZJtJx185IZhkbzwkgAAAARGVhZCBCZWVmIChXb3JtaG9sZSkAAAAAAAAAAAAAAAAKAAAAQkVFRgAAAAAAAMgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"base64"
],
"owner": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",
"executable": false,
"rentEpoch": 0
}
}

View File

@ -0,0 +1,13 @@
{
"pubkey": "HKa8mGMJJm2qnja4xmMaoeGqF4LBpGCyXVZuaiGYFaYY",
"account": {
"lamports": 1461600,
"data": [
"AQAAAP0KziEuVYQFCsdn5PTCokaUsJwqy3LxSbhyCV219Z2dAAAAAAAAAAAIAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
"base64"
],
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"executable": false,
"rentEpoch": 0
}
}

View File

@ -50,3 +50,10 @@ export const ETHEREUM_TOKEN_BRIDGE_ADDRESS =
export const WETH_ADDRESS = "0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E";
export const ETHEREUM_NFT_BRIDGE_ADDRESS =
"0x26b4afb60d6c903165150c6f0aa14f8016be4aec";
// preloaded
export const DEADBEEF_ADDRESS = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef";
export const DEADBEEF_MINT_ADDRESS =
"HKa8mGMJJm2qnja4xmMaoeGqF4LBpGCyXVZuaiGYFaYY";
export const DEADBEEF_METADATA_ADDRESS =
"A2mcN1tknMBAgMnCUF6ZT8tEJj1QE1AuCsquvXHgpWVJ";

View File

@ -55,11 +55,60 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@metaplex-foundation/beet-solana@^0.4.0":
version "0.4.0"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet-solana/-/beet-solana-0.4.0.tgz#52891e78674aaa54e0031f1bca5bfbc40de12e8d"
integrity sha512-B1L94N3ZGMo53b0uOSoznbuM5GBNJ8LwSeznxBxJ+OThvfHQ4B5oMUqb+0zdLRfkKGS7Q6tpHK9P+QK0j3w2cQ==
dependencies:
"@metaplex-foundation/beet" ">=0.1.0"
"@solana/web3.js" "^1.56.2"
bs58 "^5.0.0"
debug "^4.3.4"
"@metaplex-foundation/beet@>=0.1.0", "@metaplex-foundation/beet@^0.7.1":
version "0.7.1"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/beet/-/beet-0.7.1.tgz#0975314211643f87b5f6f3e584fa31abcf4c612c"
integrity sha512-hNCEnS2WyCiYyko82rwuISsBY3KYpe828ubsd2ckeqZr7tl0WVLivGkoyA/qdiaaHEBGdGl71OpfWa2rqL3DiA==
dependencies:
ansicolors "^0.3.2"
bn.js "^5.2.0"
debug "^4.3.3"
"@metaplex-foundation/cusper@^0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz#dc2032a452d6c269e25f016aa4dd63600e2af975"
integrity sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA==
"@metaplex-foundation/mpl-token-metadata@^2.11.1":
version "2.11.1"
resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.11.1.tgz#b7755c5cc7bae5e98e285dd675fa65b62e9bd881"
integrity sha512-FNJhDAFmpXD5K9lstJYXROjUjHQmCHFpzVs4asUpVvtkF645+PGyDtqoUENfVEwoUPY8ZT6Exs7d0exRgYqxUA==
dependencies:
"@metaplex-foundation/beet" "^0.7.1"
"@metaplex-foundation/beet-solana" "^0.4.0"
"@metaplex-foundation/cusper" "^0.0.2"
"@solana/spl-token" "^0.3.6"
"@solana/web3.js" "^1.66.2"
bn.js "^5.2.0"
debug "^4.3.4"
"@noble/curves@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932"
integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw==
dependencies:
"@noble/hashes" "1.3.0"
"@noble/ed25519@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@noble/ed25519/-/ed25519-1.7.0.tgz#583ac38340a479314b9e348d4572101ed9492f9d"
integrity sha512-LeAxFK0+181zQOhOUuKE8Jnd3duzYhDNd3iCLxpmzA5K+e4I1FdbrK3Ot0ZHBwZMeRD/6EojyUfTbpHZ+hkQHg==
"@noble/hashes@1.3.0", "@noble/hashes@^1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1"
integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg==
"@noble/hashes@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.1.2.tgz#e9e035b9b166ca0af657a7848eb2718f0f22f183"
@ -125,6 +174,15 @@
"@solana/buffer-layout-utils" "^0.2.0"
"@solana/web3.js" "^1.41.0"
"@solana/spl-token@^0.3.6":
version "0.3.7"
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.7.tgz#6f027f9ad8e841f792c32e50920d9d2e714fc8da"
integrity sha512-bKGxWTtIw6VDdCBngjtsGlKGLSmiu/8ghSt/IOYJV24BsymRbgq7r12GToeetpxmPaZYLddKwAz7+EwprLfkfg==
dependencies:
"@solana/buffer-layout" "^4.0.0"
"@solana/buffer-layout-utils" "^0.2.0"
buffer "^6.0.3"
"@solana/web3.js@^1.32.0", "@solana/web3.js@^1.41.0", "@solana/web3.js@^1.53.0":
version "1.53.0"
resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.53.0.tgz"
@ -170,6 +228,27 @@
rpc-websockets "^7.5.0"
superstruct "^0.14.2"
"@solana/web3.js@^1.56.2", "@solana/web3.js@^1.66.2":
version "1.76.0"
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.76.0.tgz#0f888e25d727d0dadf3dd8a01967347555200b2b"
integrity sha512-aJtF/nTs+9St+KtTK/wgVJ+SinfjYzn+3w1ygYIPw8ST6LH+qHBn8XkodgDTwlv/xzNkaVz1kkUDOZ8BPXyZWA==
dependencies:
"@babel/runtime" "^7.12.5"
"@noble/curves" "^1.0.0"
"@noble/hashes" "^1.3.0"
"@solana/buffer-layout" "^4.0.0"
agentkeepalive "^4.2.1"
bigint-buffer "^1.1.5"
bn.js "^5.0.0"
borsh "^0.7.0"
bs58 "^4.0.1"
buffer "6.0.3"
fast-stable-stringify "^1.0.0"
jayson "^3.4.4"
node-fetch "^2.6.7"
rpc-websockets "^7.5.1"
superstruct "^0.14.2"
"@tsconfig/node10@^1.0.7":
version "1.0.9"
resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz"
@ -283,6 +362,15 @@ acorn@^8.4.1:
resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz"
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
agentkeepalive@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255"
integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==
dependencies:
debug "^4.1.0"
depd "^2.0.0"
humanize-ms "^1.2.1"
ansi-colors@4.1.1:
version "4.1.1"
resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz"
@ -300,6 +388,11 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0:
dependencies:
color-convert "^2.0.1"
ansicolors@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
integrity sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==
anymatch@~3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz"
@ -340,6 +433,11 @@ base-x@^3.0.2:
dependencies:
safe-buffer "^5.0.1"
base-x@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==
base64-js@^1.3.1, base64-js@^1.5.1:
version "1.5.1"
resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
@ -420,6 +518,13 @@ bs58@^4.0.0, bs58@^4.0.1:
dependencies:
base-x "^3.0.2"
bs58@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279"
integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==
dependencies:
base-x "^4.0.0"
buffer-from@^1.0.0, buffer-from@^1.1.0:
version "1.1.2"
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
@ -438,6 +543,14 @@ buffer@6.0.1:
base64-js "^1.3.1"
ieee754 "^1.2.1"
buffer@6.0.3, buffer@^6.0.3, buffer@~6.0.3:
version "6.0.3"
resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
buffer@^5.4.3:
version "5.7.1"
resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz"
@ -446,14 +559,6 @@ buffer@^5.4.3:
base64-js "^1.3.1"
ieee754 "^1.1.13"
buffer@^6.0.3, buffer@~6.0.3:
version "6.0.3"
resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"
bufferutil@^4.0.1:
version "4.0.6"
resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.6.tgz"
@ -577,6 +682,13 @@ debug@4.3.3:
dependencies:
ms "2.1.2"
debug@^4.1.0, debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
dependencies:
ms "2.1.2"
decamelize@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz"
@ -594,6 +706,11 @@ delay@^5.0.0:
resolved "https://registry.npmjs.org/delay/-/delay-5.0.0.tgz"
integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==
depd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
diff@5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz"
@ -768,6 +885,13 @@ hmac-drbg@^1.0.1:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
dependencies:
ms "^2.0.0"
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
@ -1018,7 +1142,7 @@ ms@2.1.2:
resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@2.1.3:
ms@2.1.3, ms@^2.0.0:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@ -1048,6 +1172,13 @@ node-fetch@2, node-fetch@2.6.7:
dependencies:
whatwg-url "^5.0.0"
node-fetch@^2.6.7:
version "2.6.11"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.11.tgz#cde7fc71deef3131ef80a738919f999e6edfff25"
integrity sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==
dependencies:
whatwg-url "^5.0.0"
node-gyp-build@^4.2.0, node-gyp-build@^4.3.0:
version "4.5.0"
resolved "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.5.0.tgz"
@ -1167,6 +1298,19 @@ rpc-websockets@^7.5.0:
bufferutil "^4.0.1"
utf-8-validate "^5.0.2"
rpc-websockets@^7.5.1:
version "7.5.1"
resolved "https://registry.yarnpkg.com/rpc-websockets/-/rpc-websockets-7.5.1.tgz#e0a05d525a97e7efc31a0617f093a13a2e10c401"
integrity sha512-kGFkeTsmd37pHPMaHIgN1LVKXMi0JD782v4Ds9ZKtLlwdTKjn+CxM9A9/gLT2LaOuEcEFGL98h1QWQtlOIdW0w==
dependencies:
"@babel/runtime" "^7.17.2"
eventemitter3 "^4.0.7"
uuid "^8.3.2"
ws "^8.5.0"
optionalDependencies:
bufferutil "^4.0.1"
utf-8-validate "^5.0.2"
safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
version "5.2.1"
resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"