Skip to content

Instantly share code, notes, and snippets.

@ngundotra
Last active November 20, 2023 19:35
Show Gist options
  • Save ngundotra/4e1b22e0bfa34c9390007911226ed207 to your computer and use it in GitHub Desktop.
Save ngundotra/4e1b22e0bfa34c9390007911226ed207 to your computer and use it in GitHub Desktop.
tensorBuySell.ts
import { AnchorProvider, Wallet } from "@project-serum/anchor";
import {
Connection,
Keypair,
MessageV0,
PublicKey,
TransactionInstruction,
VersionedMessage,
VersionedTransaction,
AccountMeta,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import { TCompSDK } from "@tensor-oss/tcomp-sdk";
import { BN } from "bn.js";
import { homedir } from "os";
import { readFileSync } from "fs";
import {
ConcurrentMerkleTreeAccount,
MerkleTree,
} from "@solana/spl-account-compression";
import { keccak_256 } from "js-sha3";
import { publicKey, publicKeyBytes } from "@metaplex-foundation/umi";
import { createUmi } from "@metaplex-foundation/umi-bundle-defaults";
import {
mplBubblegum,
getAssetWithProof,
transfer,
hashMetadataData,
MetadataArgs,
} from "@metaplex-foundation/mpl-bubblegum";
import { getMetadataArgsSerializer } from "@metaplex-foundation/mpl-bubblegum";
/** Version from metaplex but without seller fee basis points */
export function computeMetadataArgsHash(metadata: MetadataArgs): Buffer {
const serializer = getMetadataArgsSerializer();
const serializedMetadata = serializer.serialize(metadata);
return Buffer.from(keccak_256.digest(serializedMetadata));
}
const HELIUS_API_KEY: string = "<API-KEY>";
const URL = `https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}`;
const conn = new Connection(URL);
const umi = createUmi(URL).use(mplBubblegum());
const provider = new AnchorProvider(conn, new Wallet(Keypair.generate()), {
skipPreflight: true,
commitment: "confirmed",
});
const tcompSdk = new TCompSDK({ provider });
type Creator = {
address: PublicKey;
verified: boolean;
share: number;
};
type MerkleInfo = {
root: number[];
proof: Buffer[];
leaf: PublicKey;
merkleTree: PublicKey;
index: number;
canopyDepth: number;
metaHash: Buffer;
dataHash: Buffer;
creatorsHash: Buffer;
creators: Creator[];
sellerFeeBasisPoints: number;
ownerPk: PublicKey;
};
async function transferCNFT(
ownerKp: Keypair,
destination: PublicKey,
assetId: string
): Promise<string> {
const assetWithProof = await getAssetWithProof(umi, publicKey(assetId));
const result = transfer(umi, {
...assetWithProof,
leafOwner: publicKey(ownerKp.publicKey),
newLeafOwner: publicKey(destination),
}).getInstructions();
const txid = await sendTransaction(
result.map(
(ix) =>
new TransactionInstruction({
data: Buffer.from(ix.data),
keys: ix.keys.map((meta) => {
return {
isSigner: meta.isSigner,
isWritable: meta.isWritable,
pubkey: new PublicKey(meta.pubkey.toString()),
};
}),
programId: new PublicKey(ix.programId.toString()),
})
),
ownerKp
);
return txid;
}
// Get merkle tree and proof info
async function getMerkleInfo(
assetId: string,
verify: boolean = false
): Promise<MerkleInfo> {
const assetWProof = await getAssetWithProof(umi, publicKey(assetId));
const merkleTree = new PublicKey(assetWProof.merkleTree.toString());
let account = await ConcurrentMerkleTreeAccount.fromAccountAddress(
conn,
merkleTree,
{ commitment: "confirmed" }
);
const canopyDepth = account.getCanopyDepth();
const proof = assetWProof.proof.map((p) => new PublicKey(p).toBuffer());
const leaf = new PublicKey(assetWProof.rpcAsset.compression.asset_hash);
const leafIndex = assetWProof.index;
if (verify) {
MerkleTree.verify(
Buffer.from(assetWProof.root),
{
root: Buffer.from(assetWProof.root),
proof,
leaf: leaf.toBuffer(),
leafIndex,
},
false
);
}
return {
root: Array.from(assetWProof.root),
proof,
leaf,
ownerPk: new PublicKey(assetWProof.leafOwner.toString()),
merkleTree,
index: leafIndex,
canopyDepth,
// THIS IS A SPECIFIC FIELD TO TENSOR's COMPRESSED MARKETPLACE
// THIS IS NOT AVAILABLE FROM THE DAS API NOR IS IT DERIVABLE
// FROM THE MPL-BUBBLEGUM LIBRARY. DO NOT DELETE.
metaHash: computeMetadataArgsHash(assetWProof.metadata),
dataHash: Buffer.from(assetWProof.dataHash),
creatorsHash: Buffer.from(assetWProof.creatorHash),
creators: assetWProof.metadata.creators.map((c) => {
return {
...c,
address: new PublicKey(c.address.toString()),
};
}),
sellerFeeBasisPoints: assetWProof.metadata.sellerFeeBasisPoints,
};
}
async function listAsset(
ownerKp: Keypair,
priceLamports: number,
merkleInfo: MerkleInfo
) {
const ownerPk = ownerKp.publicKey;
console.log({ ownerPk: ownerPk.toBase58() });
const {
tx: { ixs },
} = await tcompSdk.list({
// Retrieve these fields from DAS API
merkleTree: merkleInfo.merkleTree,
creatorsHash: merkleInfo.creatorsHash,
dataHash: merkleInfo.dataHash,
root: merkleInfo.root,
proof: merkleInfo.proof,
canopyDepth: merkleInfo.canopyDepth,
index: merkleInfo.index,
delegate: ownerPk,
owner: ownerPk,
payer: ownerPk,
amount: new BN(priceLamports), // in lamports
// expireInSec: expireIn ? new BN(expireIn) : null, // seconds until listing expires
expireInSec: null,
privateTaker: undefined, // optional: only this wallet can buy this listing
});
return await sendTransaction(ixs, ownerKp);
}
async function buyAsset(
payerKp: Keypair,
originalOwner: PublicKey,
priceLamports: number,
merkleInfo: MerkleInfo
) {
const payerPk = payerKp.publicKey;
const {
tx: { ixs },
} = await tcompSdk.buy({
// Retrieve these fields from DAS API
merkleTree: merkleInfo.merkleTree,
root: merkleInfo.root,
canopyDepth: merkleInfo.canopyDepth,
index: merkleInfo.index,
proof: merkleInfo.proof.map((p) =>
new PublicKey(publicKey(p).toString()).toBuffer()
),
sellerFeeBasisPoints: merkleInfo.sellerFeeBasisPoints,
metaHash: merkleInfo.metaHash,
creators: merkleInfo.creators,
payer: payerPk,
buyer: payerPk,
owner: originalOwner,
maxAmount: new BN(priceLamports),
optionalRoyaltyPct: 100, // currently required to be 100% (enforced)
});
return await sendTransaction(ixs, payerKp);
}
async function sendTransaction(
ixs: TransactionInstruction[],
payer: Keypair
): Promise<string> {
const { lastValidBlockHeight, blockhash } = await conn.getLatestBlockhash();
const message = MessageV0.compile({
recentBlockhash: blockhash,
instructions: ixs,
payerKey: payer.publicKey,
});
const tx = new VersionedTransaction(message);
tx.sign([payer]);
const txid = await conn.sendTransaction(tx, {
skipPreflight: true,
});
console.log(txid);
await conn.confirmTransaction(
{ signature: txid, blockhash, lastValidBlockHeight },
"confirmed"
);
const txResp = await conn.getTransaction(txid, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0,
});
if (txResp && txResp.meta && txResp.meta.err) {
throw new Error(JSON.stringify(txResp.meta.err));
}
return txid;
}
// Listing cNFT
async function main(assetId: string, action: "buy" | "list") {
const merkleInfo = await getMerkleInfo(assetId);
const priceLamports = LAMPORTS_PER_SOL * 0.05;
const kpFile = homedir() + "/.config/solana/id.json";
// const kpFile = "kp.json";
const ownerKp = Keypair.fromSecretKey(
Buffer.from(JSON.parse(readFileSync(kpFile).toString()))
);
try {
let txid: string;
if (action === "list") {
if (merkleInfo.ownerPk.toBase58() !== ownerKp.publicKey.toBase58()) {
throw new Error(
`Owner mismatch: ${merkleInfo.ownerPk.toBase58()} but expected ${ownerKp.publicKey.toBase58()}`
);
}
txid = await listAsset(ownerKp, priceLamports, merkleInfo);
} else {
/*
TODO: derive the owner from the asset (requires deriving the listing ID)
https://github.com/tensor-hq/tcomp-sdk/blob/ab35c35ee9fa1f941b48706dcbce36a9e452b37a/src/tcomp/pda.ts#L25
*/
txid = await buyAsset(
ownerKp,
/* TODO: this should be derived from the listing ID */
new PublicKey("6xb8JhMW5j1zZ9Rdex5wRYpjrfbG8BzM4BJ9wNn4eRpV"),
priceLamports,
merkleInfo
);
}
console.log(txid);
} catch (e) {
console.log(e);
}
}
main("DGzX4hujyDkeiJNadHU9Kif4YAkcboCQawJ5L2LXLq3H", "buy").then(() => {
console.log("Yeehaw");
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment