Last active
November 20, 2023 19:35
-
-
Save ngundotra/4e1b22e0bfa34c9390007911226ed207 to your computer and use it in GitHub Desktop.
tensorBuySell.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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