From 6da4baeef00a0df96d2f65ee72ce65d1ec7c3ccf Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 23 Oct 2024 14:51:37 -0700 Subject: [PATCH 01/35] set up test skeleton --- tests/mmm-cnft.spec.ts | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/mmm-cnft.spec.ts diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts new file mode 100644 index 0000000..34a838c --- /dev/null +++ b/tests/mmm-cnft.spec.ts @@ -0,0 +1,9 @@ +import * as anchor from '@project-serum/anchor'; + +describe('cnft tests', () => { + const endpoint = 'http://localhost:8899'; + const conn = new anchor.web3.Connection(endpoint, 'processed'); + it.only('cnft fulfill buy', async () => { + console.log("cnft fulfill buy!!!!!!!!"); + }); +}); From 0990f00929ff6daf6f710e0ddf847efac4d134b7 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 23 Oct 2024 14:57:06 -0700 Subject: [PATCH 02/35] import mpl bubblegum --- package.json | 3 +- yarn.lock | 538 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 525 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 6c62133..7af82b6 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "@solana/spl-token-group": "^0.0.1", "@solana/web3.js": "^1.65.0", "borsh": "^0.7.0", - "old-mpl-token-metadata": "npm:@metaplex-foundation/mpl-token-metadata@2.12.0" + "old-mpl-token-metadata": "npm:@metaplex-foundation/mpl-token-metadata@2.12.0", + "@metaplex-foundation/mpl-bubblegum": "^4.2.1" }, "devDependencies": { "@magiceden-oss/mmm": "file:sdk", diff --git a/yarn.lock b/yarn.lock index 81a1a4b..58304ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1066,6 +1066,13 @@ resolved "https://registry.yarnpkg.com/@metaplex-foundation/cusper/-/cusper-0.0.2.tgz#dc2032a452d6c269e25f016aa4dd63600e2af975" integrity sha512-S9RulC2fFCFOQraz61bij+5YCHhSO9llJegK8c8Y6731fSi6snUSQJdCUqYS8AIgR0TKbQvdvgSyIIdbDFZbBA== +"@metaplex-foundation/digital-asset-standard-api@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/digital-asset-standard-api/-/digital-asset-standard-api-1.0.4.tgz#a321ed36e8aa5fc988faea8e38288fdc8690dca7" + integrity sha512-YSYyMnIoKNykDZTXsSCeiIOJ7NT5Ke2pzghXDsinRwHvwIZWv+zY5kJQBvTglAzYlt/GaI+noAhUZXXmSbp07A== + dependencies: + package.json "^2.0.1" + "@metaplex-foundation/js@^0.16.1": version "0.16.1" resolved "https://registry.yarnpkg.com/@metaplex-foundation/js/-/js-0.16.1.tgz#02bec9357c48ca1d07c941534ce38c369a9bb301" @@ -1177,6 +1184,17 @@ bn.js "^5.2.0" js-sha3 "^0.8.0" +"@metaplex-foundation/mpl-bubblegum@^4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-bubblegum/-/mpl-bubblegum-4.2.1.tgz#248969b2a7e8d892113f4e2fc53608b4117cb67f" + integrity sha512-r9kHrVmkzJApbXwd7cmJyO0mAV3qsJaTjv5ks6PUT1Bzjj9QCvlJYg2UYQJLUTcrY5TjE9wXLpwUqNgllXH/Cw== + dependencies: + "@metaplex-foundation/digital-asset-standard-api" "^1.0.4" + "@metaplex-foundation/mpl-token-metadata" "3.2.1" + "@metaplex-foundation/mpl-toolbox" "^0.9.0" + "@noble/hashes" "^1.3.1" + merkletreejs "^0.3.9" + "@metaplex-foundation/mpl-candy-guard@^0.3.0": version "0.3.2" resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-candy-guard/-/mpl-candy-guard-0.3.2.tgz#426e89793676b42e9bbb5e523303fba36ccd5281" @@ -1271,6 +1289,13 @@ "@solana/web3.js" "^1.66.2" bn.js "^5.2.1" +"@metaplex-foundation/mpl-token-metadata@3.2.1", "@metaplex-foundation/mpl-token-metadata@^3.1.2": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-3.2.1.tgz#d424e378a1ee441a6431d2641d66873118d6dc67" + integrity sha512-26W1NhQwDWmLOg/pBRYut7x/vEs/5kFS2sWVEY5/X0f2jJOLhnd4NaZQcq+5u+XZsXvm1jq2AtrRGPNK43oqWQ== + dependencies: + "@metaplex-foundation/mpl-toolbox" "^0.9.4" + "@metaplex-foundation/mpl-token-metadata@^2.11.0", "@metaplex-foundation/mpl-token-metadata@^2.2.2", "@metaplex-foundation/mpl-token-metadata@^2.5.1", "@metaplex-foundation/mpl-token-metadata@^2.5.2", "@metaplex-foundation/mpl-token-metadata@^2.8.6": version "2.13.0" resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-2.13.0.tgz#ea498190ad4ed1d4c0b8218a72d03bd17a883d11" @@ -1284,14 +1309,7 @@ bn.js "^5.2.0" debug "^4.3.4" -"@metaplex-foundation/mpl-token-metadata@^3.1.2": - version "3.2.1" - resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-token-metadata/-/mpl-token-metadata-3.2.1.tgz#d424e378a1ee441a6431d2641d66873118d6dc67" - integrity sha512-26W1NhQwDWmLOg/pBRYut7x/vEs/5kFS2sWVEY5/X0f2jJOLhnd4NaZQcq+5u+XZsXvm1jq2AtrRGPNK43oqWQ== - dependencies: - "@metaplex-foundation/mpl-toolbox" "^0.9.4" - -"@metaplex-foundation/mpl-toolbox@^0.9.4": +"@metaplex-foundation/mpl-toolbox@^0.9.0", "@metaplex-foundation/mpl-toolbox@^0.9.4": version "0.9.4" resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-toolbox/-/mpl-toolbox-0.9.4.tgz#2211b2f726b1e5745c03908d26fd8ee580838b6f" integrity sha512-fd6JxfoLbj/MM8FG2x91KYVy1U6AjBQw4qjt7+Da3trzQaWnSaYHDcYRG/53xqfvZ9qofY1T2t53GXPlD87lnQ== @@ -1466,7 +1484,7 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== -"@noble/hashes@1.5.0", "@noble/hashes@^1.1.3", "@noble/hashes@^1.4.0": +"@noble/hashes@1.5.0", "@noble/hashes@^1.1.3", "@noble/hashes@^1.3.1", "@noble/hashes@^1.4.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== @@ -2160,6 +2178,13 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" +abs@^1.2.1: + version "1.3.14" + resolved "https://registry.yarnpkg.com/abs/-/abs-1.3.14.tgz#7b078d5d0735082d5bfb23d45c2d9f440a5c2222" + integrity sha512-PrS26IzwKLWwuURpiKl8wRmJ2KdR/azaVrLEBWG/TALwT20Y7qjtYp1qcMLHA4206hBHY5phv3w4pjf9NPv4Vw== + dependencies: + ul "^5.0.0" + aes-js@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/aes-js/-/aes-js-3.0.0.tgz#e21df10ad6c2053295bcbb8dab40b09dbea87e4d" @@ -2699,6 +2724,11 @@ capability@^0.2.5: resolved "https://registry.yarnpkg.com/capability/-/capability-0.2.5.tgz#51ad87353f1936ffd77f2f21c74633a4dea88801" integrity sha512-rsJZYVCgXd08sPqwmaIqjAd5SUTfonV0z/gDJ8D6cN8wQphky1kkAYEqQ+hmDxTw7UihvBfjUVUSY+DBEe44jg== +capture-stack-trace@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.2.tgz#1c43f6b059d4249e7f3f8724f15f048b927d3a8a" + integrity sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w== + chai@^4.3.4: version "4.5.0" resolved "https://registry.yarnpkg.com/chai/-/chai-4.5.0.tgz#707e49923afdd9b13a8b0b47d33d732d13812fd8" @@ -2876,6 +2906,18 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +create-error-class@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" + integrity sha512-gYTKKexFO3kh200H1Nit76sRwRtOY32vQd3jpAQKpLtZqyNsSQNfI4N7o3eP2wUjV35pTWKRYqFUDBvUha/Pkw== + dependencies: + capture-stack-trace "^1.0.0" + create-hash@^1.1.0, create-hash@^1.1.2: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" @@ -2938,6 +2980,11 @@ crypto-js@^3.1.9-1: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-3.3.0.tgz#846dd1cce2f68aacfa156c8578f926a609b7976b" integrity sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q== +crypto-js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + csv-generate@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/csv-generate/-/csv-generate-4.4.1.tgz#729781ace8d1b92f6bfb407d1ab9548728c55681" @@ -2989,6 +3036,11 @@ deep-eql@^4.1.3: dependencies: type-detect "^4.0.0" +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + deepmerge@^4.2.2: version "4.3.1" resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" @@ -3001,6 +3053,13 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +deffy@^2.2.1, deffy@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/deffy/-/deffy-2.2.4.tgz#53c1b5f59b58a58150b1c9de5529229875c4cc17" + integrity sha512-pLc9lsbsWjr6RxmJ2OLyvm+9l4j1yK69h+TML/gUit/t3vTijpkNGh8LioaJYTGO7F25m6HZndADcUOo2PsiUg== + dependencies: + typpy "^2.0.0" + define-data-property@^1.0.1, define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -3069,6 +3128,13 @@ dotenv@10.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== +duplexer2@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA== + dependencies: + readable-stream "^2.0.2" + duplexer@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" @@ -3122,7 +3188,14 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -error-ex@^1.3.1: +err@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/err/-/err-1.1.1.tgz#eb928e2e11a316648f782833d0f97258ba43c2f8" + integrity sha512-N97Ybd2jJHVQ+Ft3Q5+C2gM3kgygkdeQmEqbN2z15UTVyyEsIwLA1VK39O1DHEJhXbwIFcJLqm6iARNhFANcQA== + dependencies: + typpy "^2.2.0" + +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== @@ -3296,6 +3369,14 @@ eventemitter3@^5.0.1: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== +exec-limiter@^3.0.0: + version "3.2.13" + resolved "https://registry.yarnpkg.com/exec-limiter/-/exec-limiter-3.2.13.tgz#5f30f5990b9b10908512394b9b997ed066a574d0" + integrity sha512-86Ri699bwiHZVBzTzNj8gspqAhCPchg70zPVWIh3qzUOA1pUMcb272Em3LPk8AE0mS95B9yMJhtqF8vFJAn0dA== + dependencies: + limit-it "^3.0.0" + typpy "^2.1.0" + execa@5.1.1, execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" @@ -3477,6 +3558,13 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +function.name@^1.0.3: + version "1.0.13" + resolved "https://registry.yarnpkg.com/function.name/-/function.name-1.0.13.tgz#eef045abc4b5ff4e3e9d001a53ce14e090c971c6" + integrity sha512-mVrqdoy5npWZyoXl4DxCeuVF6delDcQjVS9aPdvLYlBxtMTZDR2B5GVEQEoM1jJyspCqg3C0v4ABkLE7tp9xFA== + dependencies: + noop6 "^1.0.1" + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -3513,6 +3601,43 @@ get-stream@^6.0.0: resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== +git-package-json@^1.4.0: + version "1.4.10" + resolved "https://registry.yarnpkg.com/git-package-json/-/git-package-json-1.4.10.tgz#d85f10d23c371295229dbebc94d5194b0fefda59" + integrity sha512-DRAcvbzd2SxGK7w8OgYfvKqhFliT5keX0lmSmVdgScgf1kkl5tbbo7Pam6uYoCa1liOiipKxQZG8quCtGWl/fA== + dependencies: + deffy "^2.2.1" + err "^1.1.1" + gry "^5.0.0" + normalize-package-data "^2.3.5" + oargv "^3.4.1" + one-by-one "^3.1.0" + r-json "^1.2.1" + r-package-json "^1.0.0" + tmp "0.0.28" + +git-source@^1.1.0: + version "1.1.10" + resolved "https://registry.yarnpkg.com/git-source/-/git-source-1.1.10.tgz#8da8b8a3819f8557dbabffca44805612ad466a6c" + integrity sha512-XZZ7ZgnLL35oLgM/xjnLYgtlKlxJG0FohC1kWDvGkU7s1VKGXK0pFF/g1itQEwQ3D+uTQzBnzPi8XbqOv7Wc1Q== + dependencies: + git-url-parse "^5.0.1" + +git-up@^1.0.0: + version "1.2.1" + resolved "https://registry.yarnpkg.com/git-up/-/git-up-1.2.1.tgz#264480a006b1d84261ac1fe09a3a5169c57ea19d" + integrity sha512-SRVN3rOLACva8imc7BFrB6ts5iISWKH1/h/1Z+JZYoUI7UVQM7gQqk4M2yxUENbq2jUUT09NEND5xwP1i7Ktlw== + dependencies: + is-ssh "^1.0.0" + parse-url "^1.0.0" + +git-url-parse@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-5.0.1.tgz#fe3d79c6746ae05048cfa508c81e79dddbba3843" + integrity sha512-4uSiOgrryNEMBX+gTWogenYRUh2j1D+95STTSEF2RCTgLkfJikl8c7BGr0Bn274hwuxTsbS2/FQ5pVS9FoXegQ== + dependencies: + git-up "^1.0.0" + glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -3556,11 +3681,42 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" +got@^5.0.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/got/-/got-5.7.1.tgz#5f81635a61e4a6589f180569ea4e381680a51f35" + integrity sha512-1qd54GLxvVgzuidFmw9ze9umxS3rzhdBH6Wt6BTYrTQUXTN01vGGYXwzLzYLowNx8HBH3/c7kRyvx90fh13i7Q== + dependencies: + create-error-class "^3.0.1" + duplexer2 "^0.1.4" + is-redirect "^1.0.0" + is-retry-allowed "^1.0.0" + is-stream "^1.0.0" + lowercase-keys "^1.0.0" + node-status-codes "^1.0.0" + object-assign "^4.0.1" + parse-json "^2.1.0" + pinkie-promise "^2.0.0" + read-all-stream "^3.0.0" + readable-stream "^2.0.5" + timed-out "^3.0.0" + unzip-response "^1.0.2" + url-parse-lax "^1.0.0" + graceful-fs@^4.2.9: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== +gry@^5.0.0: + version "5.0.8" + resolved "https://registry.yarnpkg.com/gry/-/gry-5.0.8.tgz#73c0d246fba4ce6e7924779670088a7d67222e7a" + integrity sha512-meq9ZjYVpLzZh3ojhTg7IMad9grGsx6rUUKHLqPnhLXzJkRQvEL2U3tQpS5/WentYTtHtxkT3Ew/mb10D6F6/g== + dependencies: + abs "^1.2.1" + exec-limiter "^3.0.0" + one-by-one "^3.0.0" + ul "^5.0.0" + has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" @@ -3633,6 +3789,11 @@ hmac-drbg@^1.0.1: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -3699,11 +3860,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ini@~1.3.0: + version "1.3.8" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + inquirer@^8.2.0: version "8.2.6" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.6.tgz#733b74888195d8d400a67ac332011b5fae5ea562" @@ -3802,6 +3968,28 @@ is-number@^7.0.0: resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== +is-redirect@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" + integrity sha512-cr/SlUEe5zOGmzvj9bUyC4LVvkNVAXu4GytXLNMr1pny+a65MpQ9IJzFHD5vi7FyJgb4qt27+eS3TuQnqB+RQw== + +is-retry-allowed@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz#d778488bd0a4666a3be8a1482b9f2baafedea8b4" + integrity sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg== + +is-ssh@^1.0.0, is-ssh@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2" + integrity sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ== + dependencies: + protocols "^2.0.1" + +is-stream@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ== + is-stream@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" @@ -3819,6 +4007,11 @@ is-unicode-supported@^0.1.0: resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -3882,6 +4075,11 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" +iterate-object@^1.1.0: + version "1.3.4" + resolved "https://registry.yarnpkg.com/iterate-object/-/iterate-object-1.3.4.tgz#fa50b1d9e58e340a7dd6b4c98c8a5e182e790096" + integrity sha512-4dG1D1x/7g8PwHS9aK6QV5V94+ZvyP4+d19qDv43EzImmrndysIl4prmJ1hWWIGCqrZHyaHBm6BSEWHOLnpoNw== + jake@^10.8.5: version "10.9.2" resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" @@ -4363,6 +4561,13 @@ leven@^3.1.0: resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== +limit-it@^3.0.0: + version "3.2.10" + resolved "https://registry.yarnpkg.com/limit-it/-/limit-it-3.2.10.tgz#a0e12007c9e7aeb46296309bca39bd7646d82887" + integrity sha512-T0NK99pHnkimldr1WUqvbGV1oWDku/xC9J/OqzJFsV1jeOS6Bwl8W7vkeQIBqwiON9dTALws+rX/XPMQqWerDQ== + dependencies: + typpy "^2.0.0" + lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" @@ -4417,6 +4622,11 @@ lower-case@^2.0.2: dependencies: tslib "^2.0.3" +lowercase-keys@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.1.tgz#6f9e30b47084d971a7c820ff15a6c5167b74c26f" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + lru-cache@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" @@ -4478,6 +4688,17 @@ merkletreejs@^0.2.32: treeify "^1.1.0" web3-utils "^1.3.4" +merkletreejs@^0.3.9: + version "0.3.11" + resolved "https://registry.yarnpkg.com/merkletreejs/-/merkletreejs-0.3.11.tgz#e0de05c3ca1fd368de05a12cb8efb954ef6fc04f" + integrity sha512-LJKTl4iVNTndhL+3Uz/tfkjD0klIWsHlUzgtuNnNrsf7bAlXR30m+xYB7lHr5Z/l6e/yAIsr26Dabx6Buo4VGQ== + dependencies: + bignumber.js "^9.0.1" + buffer-reverse "^1.0.1" + crypto-js "^4.2.0" + treeify "^1.1.0" + web3-utils "^1.3.4" + micro-ftch@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/micro-ftch/-/micro-ftch-0.3.1.tgz#6cb83388de4c1f279a034fb0cf96dfc050853c5f" @@ -4537,7 +4758,7 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.7: +minimist@^1.2.0, minimist@^1.2.7: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -4646,6 +4867,26 @@ node-releases@^2.0.18: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== +node-status-codes@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-status-codes/-/node-status-codes-1.0.0.tgz#5ae5541d024645d32a58fcddc9ceecea7ae3ac2f" + integrity sha512-1cBMgRxdMWE8KeWCqk2RIOrvUb0XCwYfEsY5/y2NlXyq4Y/RumnOZvTj4Nbr77+Vb2C+kyBoRTdkNOS8L3d/aQ== + +noop6@^1.0.1: + version "1.0.9" + resolved "https://registry.yarnpkg.com/noop6/-/noop6-1.0.9.tgz#8749944c15c09f2cd2d562ac24f5a8341762a950" + integrity sha512-DB3Hwyd89dPr5HqEPg3YHjzvwh/mCqizC1zZ8vyofqc+TQRyPDnT4wgXXbLGF4z9YAzwwTLi8pNLhGqcbSjgkA== + +normalize-package-data@^2.3.5: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" @@ -4673,6 +4914,26 @@ o3@^1.0.3: dependencies: capability "^0.2.5" +oargv@^3.4.1: + version "3.4.10" + resolved "https://registry.yarnpkg.com/oargv/-/oargv-3.4.10.tgz#facf418534c1d5238a6998246419faa4c930cfe5" + integrity sha512-SXaMANv9sr7S/dP0vj0+Ybipa47UE1ntTWQ2rpPRhC6Bsvfl+Jg03Xif7jfL0sWKOYWK8oPjcZ5eJ82t8AP/8g== + dependencies: + iterate-object "^1.1.0" + ul "^5.0.0" + +obj-def@^1.0.0: + version "1.0.9" + resolved "https://registry.yarnpkg.com/obj-def/-/obj-def-1.0.9.tgz#2e63708e91e425f11e60928db1d2d8549f6a95fa" + integrity sha512-bQ4ya3VYD6FAA1+s6mEhaURRHSmw4+sKaXE6UyXZ1XDYc5D+c7look25dFdydmLd18epUegh398gdDkMUZI9xg== + dependencies: + deffy "^2.2.2" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + object-is@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.6.tgz#1a6a53aed2dd8f7e6775ff870bea58545956ab07" @@ -4716,6 +4977,14 @@ once@^1.3.0, once@^1.4.0: dependencies: wrappy "1" +one-by-one@^3.0.0, one-by-one@^3.1.0: + version "3.2.8" + resolved "https://registry.yarnpkg.com/one-by-one/-/one-by-one-3.2.8.tgz#9254afa210e816bbb298e2addf4c62927d8f5e6d" + integrity sha512-HR/pSzZdm46Xqj58K+Bu64kMbSTw8/u77AwWvV+rprO/OsuR++pPlkUJn+SmwqBGRgHKwSKQ974V3uls7crIeQ== + dependencies: + obj-def "^1.0.0" + sliced "^1.0.1" + onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" @@ -4738,7 +5007,7 @@ ora@^5.4.1: strip-ansi "^6.0.0" wcwidth "^1.0.1" -os-tmpdir@~1.0.2: +os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== @@ -4769,11 +5038,44 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-path@^1.0.0: + version "1.0.9" + resolved "https://registry.yarnpkg.com/package-json-path/-/package-json-path-1.0.9.tgz#6caa627239b7b7fdccd4dba1026d86634397efde" + integrity sha512-uNu7f6Ef7tQHZRnkyVnCtzdSYVN9uBtge/sG7wzcUaawFWkPYUq67iXxRGrQSg/q0tzxIB8jSyIYUKjG2Jn//A== + dependencies: + abs "^1.2.1" + +package-json@^2.3.1: + version "2.4.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-2.4.0.tgz#0d15bd67d1cbbddbb2ca222ff2edb86bcb31a8bb" + integrity sha512-PRg65iXMTt/uK8Rfh5zvzkUbfAPitF17YaCY+IbHsYgksiLvtzWWTUildHth3mVaZ7871OJ7gtP4LBRBlmAdXg== + dependencies: + got "^5.0.0" + registry-auth-token "^3.0.1" + registry-url "^3.0.3" + semver "^5.1.0" + +package.json@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/package.json/-/package.json-2.0.1.tgz#f886059d2a49ed076e64883695d73b2b46d21d6d" + integrity sha512-pSxZ6XR5yEawRN2ekxx9IKgPN5uNAYco7MCPxtBEWMKO3UKWa1X2CtQMzMgloeGj2g2o6cue3Sb5iPkByIJqlw== + dependencies: + git-package-json "^1.4.0" + git-source "^1.1.0" + package-json "^2.3.1" + pako@^2.0.3: version "2.1.0" resolved "https://registry.yarnpkg.com/pako/-/pako-2.1.0.tgz#266cc37f98c7d883545d11335c00fbd4062c9a86" integrity sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug== +parse-json@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ== + dependencies: + error-ex "^1.2.0" + parse-json@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" @@ -4784,6 +5086,14 @@ parse-json@^5.2.0: json-parse-even-better-errors "^2.3.0" lines-and-columns "^1.1.6" +parse-url@^1.0.0: + version "1.3.11" + resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-1.3.11.tgz#57c15428ab8a892b1f43869645c711d0e144b554" + integrity sha512-1wj9nkgH/5EboDxLwaTMGJh3oH3f+Gue+aGdh631oCqoSBpokzmMmOldvOeBPtB8GJBYJbaF93KPzlkU+Y1ksg== + dependencies: + is-ssh "^1.3.0" + protocols "^1.4.0" + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" @@ -4842,6 +5152,18 @@ picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw== + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg== + pirates@^4.0.4: version "4.0.6" resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" @@ -4859,6 +5181,11 @@ possible-typed-array-names@^1.0.0: resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== +prepend-http@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" + integrity sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg== + prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -4880,6 +5207,11 @@ pretty-format@^29.0.0, pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" @@ -4893,6 +5225,16 @@ prompts@^2.0.1: kleur "^3.0.3" sisteransi "^1.0.5" +protocols@^1.4.0: + version "1.4.8" + resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.8.tgz#48eea2d8f58d9644a4a32caae5d5db290a075ce8" + integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== + +protocols@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" + integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== + ps-tree@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.2.0.tgz#5e7425b89508736cdd4f2224d028f7bb3f722ebd" @@ -4910,6 +5252,21 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +r-json@^1.2.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/r-json/-/r-json-1.3.0.tgz#a0b9df3455183a0d8c2de733b047839e462381c9" + integrity sha512-xesd+RHCpymPCYd9DvDvUr1w1IieSChkqYF1EpuAYrvCfLXji9NP36DvyYZJZZB5soVDvZ0WUtBoZaU1g5Yt9A== + dependencies: + w-json "1.3.10" + +r-package-json@^1.0.0: + version "1.0.9" + resolved "https://registry.yarnpkg.com/r-package-json/-/r-package-json-1.0.9.tgz#36549f0d2eafd1e998ee36c7828b0b1b7315d840" + integrity sha512-G4Vpf1KImWmmPFGdtWQTU0L9zk0SjqEC4qs/jE7AQ+Ylmr5kizMzGeC4wnHp5+ijPqNN+2ZPpvyjVNdN1CDVcg== + dependencies: + package-json-path "^1.0.0" + r-json "^1.2.1" + randombytes@^2.0.1, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" @@ -4917,11 +5274,42 @@ randombytes@^2.0.1, randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +rc@^1.0.1, rc@^1.1.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + react-is@^18.0.0: version "18.3.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== +read-all-stream@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/read-all-stream/-/read-all-stream-3.1.0.tgz#35c3e177f2078ef789ee4bfafa4373074eaef4fa" + integrity sha512-DI1drPHbmBcUDWrJ7ull/F2Qb8HkwBncVx8/RpKYFSIACYaVRQReISYPdZz/mt1y1+qMCOrfReTopERmaxtP6w== + dependencies: + pinkie-promise "^2.0.0" + readable-stream "^2.0.0" + +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -4936,6 +5324,21 @@ regenerator-runtime@^0.14.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== +registry-auth-token@^3.0.1: + version "3.4.0" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" + integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A== + dependencies: + rc "^1.1.6" + safe-buffer "^5.0.1" + +registry-url@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" + integrity sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA== + dependencies: + rc "^1.0.1" + require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" @@ -4958,7 +5361,7 @@ resolve.exports@^2.0.0: resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.2.tgz#f8c934b8e6a13f539e38b7098e2e36134f01e800" integrity sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg== -resolve@^1.20.0: +resolve@^1.10.0, resolve@^1.20.0: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -5033,6 +5436,11 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.0, resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== +safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -5052,6 +5460,11 @@ secp256k1@^4.0.2: node-addon-api "^2.0.0" node-gyp-build "^4.2.0" +"semver@2 || 3 || 4 || 5", semver@^5.1.0: + version "5.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + semver@^6.3.0, semver@^6.3.1: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -5114,6 +5527,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +sliced@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + integrity sha512-VZBmZP8WU3sMOZm1bdgTadsQbcscK0UM8oKxKVBs4XAhUo2Xxzm/OFMGBkPusxw9xL3Uy8LrzEqGqJhclsr0yA== + snake-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" @@ -5135,6 +5553,32 @@ source-map@^0.6.0, source-map@^0.6.1: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== +spdx-correct@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" + integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" + integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.20" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz#e44ed19ed318dd1e5888f93325cee800f0f51b89" + integrity sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw== + split@0.3: version "0.3.3" resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" @@ -5217,6 +5661,13 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -5246,6 +5697,11 @@ strip-json-comments@^3.1.1: resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ== + superstruct@^0.15.4: version "0.15.5" resolved "https://registry.yarnpkg.com/superstruct/-/superstruct-0.15.5.tgz#0f0a8d3ce31313f0d84c6096cd4fa1bfdedc9dab" @@ -5306,6 +5762,11 @@ through@2, "through@>=2.2.7 <3", through@^2.3.6, through@~2.3, through@~2.3.1: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== +timed-out@^3.0.0: + version "3.1.3" + resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-3.1.3.tgz#95860bfcc5c76c277f8f8326fd0f5b2e20eba217" + integrity sha512-3RB4qgvPkxF/FGPnrzaWLhW1rxNK2sdH0mFjbhxkfTR6QXvcM3EtYm9L44UrhODZrZ+yhDXeMncLqi8QXn2MJg== + tmp-promise@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-3.0.3.tgz#60a1a1cc98c988674fcbfd23b6e3367bdeac4ce7" @@ -5313,6 +5774,13 @@ tmp-promise@^3.0.2: dependencies: tmp "^0.2.0" +tmp@0.0.28: + version "0.0.28" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.28.tgz#172735b7f614ea7af39664fa84cf0de4e515d120" + integrity sha512-c2mmfiBmND6SOVxzogm1oda0OJ1HZVIk/5n26N59dDTh80MUeavpiCls4PGAdkX1PFkKokLpcf7prSjCeXLsJg== + dependencies: + os-tmpdir "~1.0.1" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -5429,16 +5897,36 @@ typescript@^4.4.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +typpy@^2.0.0, typpy@^2.1.0, typpy@^2.2.0, typpy@^2.3.4: + version "2.3.13" + resolved "https://registry.yarnpkg.com/typpy/-/typpy-2.3.13.tgz#7e16a3aa83d7eecdfbd5ee615b9ffd785887ee7e" + integrity sha512-vOxIcQz9sxHi+rT09SJ5aDgVgrPppQjwnnayTrMye1ODaU8gIZTDM19t9TxmEElbMihx2Nq/0/b/MtyKfayRqA== + dependencies: + function.name "^1.0.3" + u3@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/u3/-/u3-0.1.1.tgz#5f52044f42ee76cd8de33148829e14528494b73b" integrity sha512-+J5D5ir763y+Am/QY6hXNRlwljIeRMZMGs0cT6qqZVVzzT3X3nFPXVyPOFRMOR4kupB0T8JnCdpWdp6Q/iXn3w== +ul@^5.0.0: + version "5.2.15" + resolved "https://registry.yarnpkg.com/ul/-/ul-5.2.15.tgz#426425355ae15df2d5d09b351aade26ed06dd9ed" + integrity sha512-svLEUy8xSCip5IWnsRa0UOg+2zP0Wsj4qlbjTmX6GJSmvKMHADBuHOm1dpNkWqWPIGuVSqzUkV3Cris5JrlTRQ== + dependencies: + deffy "^2.2.2" + typpy "^2.3.4" + undici-types@~6.19.2: version "6.19.8" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +unzip-response@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-1.0.2.tgz#b984f0877fc0a89c2c773cc1ef7b5b232b5b06fe" + integrity sha512-pwCcjjhEcpW45JZIySExBHYv5Y9EeL2OIGEfrSKp2dMUFGFv4CpvZkwJbVge8OvGH2BNNtJBx67DuKuJhf+N5Q== + update-browserslist-db@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" @@ -5447,6 +5935,13 @@ update-browserslist-db@^1.1.0: escalade "^3.1.2" picocolors "^1.0.1" +url-parse-lax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" + integrity sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA== + dependencies: + prepend-http "^1.0.1" + utf-8-validate@^5.0.2: version "5.0.10" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" @@ -5459,7 +5954,7 @@ utf8@3.0.0: resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== -util-deprecate@^1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -5489,11 +5984,24 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^2.0.0" +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + vlq@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/vlq/-/vlq-2.0.4.tgz#6057b85729245b9829e3cc7755f95b228d4fe041" integrity sha512-aodjPa2wPQFkra1G8CzJBTHXhgk3EVSwxSWXNPr1fgdFLUb8kvLV1iEb6rFgasIsjP82HWI6dsb5Io26DDnasA== +w-json@1.3.10: + version "1.3.10" + resolved "https://registry.yarnpkg.com/w-json/-/w-json-1.3.10.tgz#ac448a19ca22376e2753a684b52369c7b1e83313" + integrity sha512-XadVyw0xE+oZ5FGApXsdswv96rOhStzKqL53uSe5UaTadABGkWIg1+DTx8kiZ/VqTZTBneoL0l65RcPe4W3ecw== + wait-on@7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/wait-on/-/wait-on-7.0.1.tgz#5cff9f8427e94f4deacbc2762e6b0a489b19eae9" From 79df2f41ed7e3ad175a258b6039d48cda7880101 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 23 Oct 2024 15:22:13 -0700 Subject: [PATCH 03/35] basic cnft test set up --- Anchor.toml | 9 + sdk/src/cnft.ts | 38 +++++ sdk/src/index.ts | 1 + tests/mmm-cnft.spec.ts | 41 ++++- tests/mmm-ext-fulfill.spec.ts | 2 +- tests/mmm-ocp.spec.ts | 2 +- tests/utils/cnft.ts | 307 ++++++++++++++++++++++++++++++++++ tests/utils/index.ts | 1 + 8 files changed, 398 insertions(+), 3 deletions(-) create mode 100644 sdk/src/cnft.ts create mode 100644 tests/utils/cnft.ts diff --git a/Anchor.toml b/Anchor.toml index fc8bcce..a1e6719 100644 --- a/Anchor.toml +++ b/Anchor.toml @@ -39,6 +39,15 @@ address = "CZ1rQoAHSqWBoAEfqGsiLhgbM59dDrCWk3rnG5FXaoRV" # libreplex royalty enf [[test.validator.clone]] address = "CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d" # metaplex core program +[[test.validator.clone]] +address = "BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY" # bubblegum + +[[test.validator.clone]] +address = "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV" # Noop logger for bubblegum + +[[test.validator.clone]] +address = "cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK" # compression program + [[test.validator.account]] address = "9V5HWD1ap6mCDMhBoXU5SVcZZn9ihqJtoMQZsw5MTnoD" # example payment proxy filename = './tests/deps/proxy.json' diff --git a/sdk/src/cnft.ts b/sdk/src/cnft.ts new file mode 100644 index 0000000..787c502 --- /dev/null +++ b/sdk/src/cnft.ts @@ -0,0 +1,38 @@ +import { PublicKey } from '@solana/web3.js'; + +export interface CNFT { + nftIndex: number; + proofs: PublicKey[]; +} + +export interface BubblegumTreeRef { + merkleTree: PublicKey; + // The Merkle root for the tree. Can be retrieved from off-chain data store. + root: PublicKey; + // The Keccak256 hash of the NFTs existing metadata (without the verified flag for the creator changed). + // The metadata is retrieved from off-chain data store + // Hash(Hash(metadataArgs), seller_fee_basis_points) + dataHash: PublicKey; + // The Keccak256 hash of the NFTs existing creators array (without the verified flag for the creator changed). + // The creators array is retrieved from off-chain data store. + creatorHash: PublicKey; + // The Keccak256 hash of the NFT metadata: + // Hash(metadataArgs) + metadataHash: number[]; + // A nonce ("number used once") value used to make the Merkle tree leaves unique. + // This is the value of num_minted for the tree stored in the TreeConfig account at the time the NFT was minted. + // The unique value for each asset can be retrieved from off-chain data store. + nonce: number; +} + +export interface BubblegumNftArgs { + tree: BubblegumTreeRef; + nft: CNFT; +} + +export interface CnftDepositSellArgs { + nft: BubblegumNftArgs; + seller: PublicKey; + nftDelegate: PublicKey; // delegate for the NFT, required to be correctly for cNFT transfers + price: number; +} \ No newline at end of file diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 4a0c3d5..b0f15c3 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -5,3 +5,4 @@ export * from './mmmClient'; export * from './pda'; export * from './price'; export * from './transferHookProvider'; +export * from './cnft'; diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 34a838c..7fe040a 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -1,9 +1,48 @@ import * as anchor from '@project-serum/anchor'; +import { generateSigner, sol, Umi } from '@metaplex-foundation/umi'; +import { + createUmi, + getPubKey, + setupTreeAndListing, + verifyOwnership, +} from './utils'; describe('cnft tests', () => { const endpoint = 'http://localhost:8899'; const conn = new anchor.web3.Connection(endpoint, 'processed'); + let umi: Umi; + + beforeAll(async () => { + umi = await createUmi(endpoint, sol(3)); + }); + it.only('cnft fulfill buy', async () => { - console.log("cnft fulfill buy!!!!!!!!"); + const umi = await createUmi(endpoint, sol(3)); + const seller = generateSigner(umi); + await umi.rpc.airdrop(seller.publicKey, sol(1)); + await umi.rpc.airdrop(seller.publicKey, sol(10)); + + const { + merkleTree, + escrowedProof, + leafIndex, + metadata, + getBubblegumTreeRef, + getCnftRef, + } = await setupTreeAndListing(umi, seller); + + const merkleyTreePubkey = getPubKey(merkleTree); + + console.log('merkleyTreePubkey', merkleyTreePubkey.toBase58()); + + // Verify that buyer now owns the cNFT. + await verifyOwnership( + umi, + merkleTree, + seller.publicKey, + leafIndex, + metadata, + [], + ); }); }); diff --git a/tests/mmm-ext-fulfill.spec.ts b/tests/mmm-ext-fulfill.spec.ts index f19710b..6f8e951 100644 --- a/tests/mmm-ext-fulfill.spec.ts +++ b/tests/mmm-ext-fulfill.spec.ts @@ -43,7 +43,7 @@ import { sendAndAssertTx, } from './utils'; -describe('mmm-ext-fulfill', () => { +describe.skip('mmm-ext-fulfill', () => { const { connection } = anchor.AnchorProvider.env(); const wallet = new anchor.Wallet(Keypair.generate()); const provider = new anchor.AnchorProvider(connection, wallet, { diff --git a/tests/mmm-ocp.spec.ts b/tests/mmm-ocp.spec.ts index c9b2714..7d57480 100644 --- a/tests/mmm-ocp.spec.ts +++ b/tests/mmm-ocp.spec.ts @@ -42,7 +42,7 @@ import { PROGRAM_ID as OCP_PROGRAM_ID, } from '@magiceden-oss/open_creator_protocol'; -describe('mmm-ocp', () => { +describe.skip('mmm-ocp', () => { const { connection } = anchor.AnchorProvider.env(); const wallet = new anchor.Wallet(Keypair.generate()); const provider = new anchor.AnchorProvider(connection, wallet, { diff --git a/tests/utils/cnft.ts b/tests/utils/cnft.ts new file mode 100644 index 0000000..e0f3a2d --- /dev/null +++ b/tests/utils/cnft.ts @@ -0,0 +1,307 @@ +import { + Creator, + MetadataArgsArgs, + mplBubblegum, + createTree as baseCreateTree, + mintV1 as baseMintV1, + fetchMerkleTree, + findLeafAssetIdPda, + hashLeaf, + hashMetadataCreators, + verifyCreator, + getCurrentRoot, + hashMetadataData, + hash, + getMetadataArgsSerializer, + getMerkleProof, + verifyLeaf, + } from '@metaplex-foundation/mpl-bubblegum'; + import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; + import { + Context, + generateSigner, + KeypairSigner, + none, + Pda, + PublicKey, + publicKey, + sol, + SolAmount, + Umi, + PublicKey as UmiPublicKey, + } from '@metaplex-foundation/umi'; + import { createUmi as baseCreateUmi } from '@metaplex-foundation/umi-bundle-tests'; + import { BubblegumTreeRef, CNFT, CnftDepositSellArgs } from '../../sdk/src'; + import { PublicKey as Web3PubKey } from '@solana/web3.js'; + + export const ME_TREASURY = new Web3PubKey( + 'rFqFJ9g7TGBD8Ed7TPDnvGKZ5pWLPDyxLcvcH2eRCtt', + ); + + export const treasury = publicKey(ME_TREASURY.toBase58()); + + export const createUmi = async (endpoint?: string, airdropAmount?: SolAmount) => + (await baseCreateUmi(endpoint, undefined, airdropAmount)) + .use(mplTokenMetadata()) + .use(mplBubblegum()); + + export const createTree = async ( + context: Context, + input: Partial[1]> = {}, + ): Promise => { + const merkleTree = generateSigner(context); + const builder = await baseCreateTree(context, { + merkleTree, + maxDepth: 14, + maxBufferSize: 64, + ...input, + }); + await builder.sendAndConfirm(context); + return merkleTree.publicKey; + }; + + export async function getCreatorPair(umi: Umi): Promise { + const creator1 = generateSigner(umi); + const creator2 = generateSigner(umi); + await umi.rpc.airdrop(creator1.publicKey, sol(1)); + await umi.rpc.airdrop(creator2.publicKey, sol(1)); + return [creator1, creator2]; + } + + export async function initUnverifiedCreatorsArray( + creators: KeypairSigner[], + ): Promise { + return [ + { + address: creators[0].publicKey, + verified: false, + share: 60, + }, + { + address: creators[1].publicKey, + verified: false, + share: 40, + }, + ]; + } + + export const mint = async ( + context: Context, + input: Omit[1], 'metadata' | 'leafOwner'> & { + leafIndex?: number | bigint; + metadata?: Partial[1]['metadata']>; + leafOwner?: PublicKey; + creators?: Parameters[1]['metadata']['creators']; + }, + ): Promise<{ + metadata: MetadataArgsArgs; + assetId: Pda; + leaf: PublicKey; + leafIndex: number; + creatorsHash: PublicKey; + }> => { + const merkleTree = publicKey(input.merkleTree, false); + const leafOwner = input.leafOwner ?? context.identity.publicKey; + const leafIndex = Number( + input.leafIndex ?? + (await fetchMerkleTree(context, merkleTree)).tree.activeIndex, + ); + const leafCreators = input.creators ?? []; + const metadata: MetadataArgsArgs = { + name: 'My NFT', + uri: 'https://example.com/my-nft.json', + sellerFeeBasisPoints: 500, // 5% + collection: none(), + creators: leafCreators, + ...input.metadata, + }; + + await baseMintV1(context, { + ...input, + metadata, + leafOwner, + }).sendAndConfirm(context); + + return { + metadata, + assetId: findLeafAssetIdPda(context, { merkleTree, leafIndex }), + leafIndex, + leaf: publicKey( + hashLeaf(context, { + merkleTree, + owner: publicKey(leafOwner, false), + delegate: publicKey(input.leafDelegate ?? leafOwner, false), + leafIndex, + metadata, + }), + ), + creatorsHash: publicKey(hashMetadataCreators(leafCreators)), + }; + }; + + // This is Hash(metadataArgs). Useful for verifying sellers fee basis points are valid. + // NOTE: this does not perform any checks on the hash, it is recommended to use getMetadataHashChecked + // in production!! + export function hashMetadataArgsArgs(metadata: MetadataArgsArgs): Uint8Array { + return hash(getMetadataArgsSerializer().serialize(metadata)); + } + + export function bufferToArray(buffer: Buffer): number[] { + const nums: number[] = []; + for (let i = 0; i < buffer.length; i++) { + nums.push(buffer[i]); + } + return nums; + } + + export function getPubKey(umiKey: UmiPublicKey) { + return new Web3PubKey(umiKey.toString()); + } + + export async function verifyOwnership( + umi: Umi, + merkleTree: UmiPublicKey, + expectedOwner: UmiPublicKey, + leafIndex: number, + metadata: MetadataArgsArgs, + preMints: { leaf: UmiPublicKey }[], + ): Promise<{ currentProof: UmiPublicKey[] }> { + const escrowedLeaf = hashLeaf(umi, { + merkleTree, + owner: expectedOwner, + leafIndex, + metadata, + }); + + const currentProof = getMerkleProof( + [...preMints.map((m) => m.leaf), publicKey(escrowedLeaf)], + 5, + publicKey(escrowedLeaf), + ); + + const merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + + await verifyLeaf(umi, { + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + leaf: escrowedLeaf, + index: leafIndex, + proof: currentProof, + }).sendAndConfirm(umi); + + return { currentProof }; + } + + export async function setupTreeAndListing( + umi: Umi, + seller: KeypairSigner, + ): Promise<{ + merkleTree: UmiPublicKey; + leaf: UmiPublicKey; + escrowedProof: UmiPublicKey[]; + creators: Creator[]; + leafIndex: number; + metadata: MetadataArgsArgs; + creatorsHash: UmiPublicKey; + getBubblegumTreeRef: () => Promise; + getCnftRef: (proof: UmiPublicKey[]) => CNFT; + }> { + const maxDepth = 5; + const merkleTree = await createTree(umi, { + maxDepth, + maxBufferSize: 8, + }); + + const creatorSigners = await getCreatorPair(umi); + const unverifiedCreators = await initUnverifiedCreatorsArray(creatorSigners); + + const { metadata, leaf, leafIndex, creatorsHash } = await mint(umi, { + merkleTree, + leafOwner: seller.publicKey, + creators: unverifiedCreators, + }); + + // Verify creator A + await verifyCreator(umi, { + leafOwner: seller.publicKey, + creator: creatorSigners[0], + merkleTree, + root: getCurrentRoot((await fetchMerkleTree(umi, merkleTree)).tree), + nonce: leafIndex, + index: leafIndex, + metadata, + proof: [], + }).sendAndConfirm(umi); + + const updatedMetadata = { + ...metadata, + creators: [ + { address: creatorSigners[0].publicKey, verified: true, share: 60 }, + { address: creatorSigners[1].publicKey, verified: false, share: 40 }, + ], + }; + const leafDataPostVerification = hashLeaf(umi, { + merkleTree, + owner: seller.publicKey, + leafIndex, + metadata: updatedMetadata, + }); + const updatedLeaf = publicKey(leafDataPostVerification); + // Make sure that the leaf is updated with the verifie creator. + const merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + expect(merkleTreeAccount.tree.rightMostPath.leaf).toEqual(updatedLeaf); + + const getBubblegumTreeRef = async () => ({ + merkleTree: getPubKey(merkleTree), + root: new Web3PubKey( + getCurrentRoot((await fetchMerkleTree(umi, merkleTree)).tree), + ), + dataHash: new Web3PubKey(hashMetadataData(updatedMetadata)), + metadataHash: bufferToArray( + Buffer.from(hashMetadataArgsArgs(updatedMetadata)), + ), + creatorHash: new Web3PubKey(hashMetadataCreators(updatedMetadata.creators)), + nonce: leafIndex, + }); + + const getCnftRef = (proof: UmiPublicKey[]) => ({ + nftIndex: leafIndex, + proofs: proof.map(getPubKey), + }); + + const proof = getMerkleProof([updatedLeaf], maxDepth, updatedLeaf); + + const sellArgs: CnftDepositSellArgs = { + nft: { + tree: await getBubblegumTreeRef(), + nft: getCnftRef(proof), + }, + seller: getPubKey(seller.publicKey), + nftDelegate: getPubKey(seller.publicKey), + price: 100, + }; + + // await runSell(umi, m3Client, seller, sellArgs); + + // Verify that M3 program owns the cNFT. + const { currentProof: escrowedProof } = await verifyOwnership( + umi, + merkleTree, + seller.publicKey, // !!!! this need to be changed to the pool? + leafIndex, + updatedMetadata, + [], + ); + + return { + merkleTree, + leaf, + escrowedProof, + leafIndex, + metadata: updatedMetadata, + creatorsHash, + creators: updatedMetadata.creators, + getBubblegumTreeRef, + getCnftRef, + }; + } \ No newline at end of file diff --git a/tests/utils/index.ts b/tests/utils/index.ts index 2db438e..60aafc7 100644 --- a/tests/utils/index.ts +++ b/tests/utils/index.ts @@ -5,3 +5,4 @@ export * from './mmm'; export * from './nfts'; export * from './ocp'; export * from './mpl_core'; +export * from './cnft'; From 0dff0f13a4d03b192006c0940d0ce3dd5546477e Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 23 Oct 2024 15:30:57 -0700 Subject: [PATCH 04/35] imported bubblegum crate --- Cargo.lock | 24 ++++++++++++++++++++++++ programs/mmm/Cargo.toml | 1 + 2 files changed, 25 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 83ca789..750e800 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1151,6 +1151,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "kaigan" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6dd100976df9dd59d0c3fecf6f9ad3f161a087374d1b2a77ebb4ad8920f11bb" +dependencies = [ + "borsh 0.10.3", +] + [[package]] name = "keccak" version = "0.1.5" @@ -1313,6 +1322,7 @@ dependencies = [ "anchor-spl", "community-managed-token", "m2_interface", + "mpl-bubblegum", "mpl-core", "mpl-token-metadata 4.1.1", "open_creator_protocol", @@ -1345,6 +1355,20 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "mpl-bubblegum" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9eff5ae5cafd1acdf7e7c93359da1eec91dcaede318470d9f68b78e8b7469f4" +dependencies = [ + "borsh 0.10.3", + "kaigan", + "num-derive 0.3.3", + "num-traits", + "solana-program", + "thiserror", +] + [[package]] name = "mpl-core" version = "0.7.0" diff --git a/programs/mmm/Cargo.toml b/programs/mmm/Cargo.toml index 60ab09d..388dd77 100644 --- a/programs/mmm/Cargo.toml +++ b/programs/mmm/Cargo.toml @@ -31,3 +31,4 @@ spl-associated-token-account = { version = "2.2.0", features = [ spl-token-2022 = {version = "1.0.0", features = ["no-entrypoint"] } m2_interface = { path = "../m2_interface" } mpl-core = "0.7.0" +mpl-bubblegum = "1.4.0" From d1c1f96d8e3d278857241f86a4bea6f8d57f2fe0 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 23 Oct 2024 15:43:36 -0700 Subject: [PATCH 05/35] fulfill buy contract skeleton --- programs/mmm/src/instructions/cnft/mod.rs | 3 + .../instructions/cnft/sol_cnft_fulfill_buy.rs | 145 ++++++++ programs/mmm/src/instructions/mod.rs | 2 + programs/mmm/src/lib.rs | 7 + programs/mmm/src/state.rs | 42 +++ sdk/src/idl/mmm.ts | 326 ++++++++++++++++++ 6 files changed, 525 insertions(+) create mode 100644 programs/mmm/src/instructions/cnft/mod.rs create mode 100644 programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs diff --git a/programs/mmm/src/instructions/cnft/mod.rs b/programs/mmm/src/instructions/cnft/mod.rs new file mode 100644 index 0000000..51651c0 --- /dev/null +++ b/programs/mmm/src/instructions/cnft/mod.rs @@ -0,0 +1,3 @@ +pub mod sol_cnft_fulfill_buy; + +pub use sol_cnft_fulfill_buy::*; \ No newline at end of file diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs new file mode 100644 index 0000000..b2531e5 --- /dev/null +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -0,0 +1,145 @@ +use std::str::FromStr; + +use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; + +use crate::{ + constants::*, + errors::MMMErrorCode, + state::{BubblegumProgram, Pool, SellState, TreeConfigAnchor}, + util::{log_pool, try_close_pool}, + verify_referral::verify_referral, +}; + +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct SolCnftFulfillBuyArgs { + // === cNFT transfer args === // + // The Merkle root for the tree. Can be retrieved from off-chain data store. + root: [u8; 32], + // The Keccak256 hash of the NFTs existing metadata (without the verified flag for the creator changed). + // The metadata is retrieved from off-chain data store. + metadata_hash: [u8; 32], + // The Keccak256 hash of the NFTs existing creators array (without the verified flag for the creator changed). + // The creators array is retrieved from off-chain data store. + creator_hash: [u8; 32], + // A nonce ("number used once") value used to make the Merkle tree leaves unique. + // This is the value of num_minted for the tree stored in the TreeConfig account at the time the NFT was minted. + // The unique value for each asset can be retrieved from off-chain data store. + nonce: u64, + // The index of the leaf in the merkle tree. Can be retrieved from off-chain store. + index: u32, + // === Contract args === // + // Price of the NFT in the payment_mint. + buyer_price: u64, + // The mint of the SPL token used to pay for the NFT, currently not used and default to SOL. + payment_mint: Pubkey, + // The asset amount to deposit, default to 1. + pub asset_amount: u64, + pub min_payment_amount: u64, + pub allowlist_aux: Option, // TODO: use it for future allowlist_aux + pub maker_fee_bp: i16, // will be checked by cosigner + pub taker_fee_bp: i16, // will be checked by cosigner +} + +#[derive(Accounts)] +#[instruction(args:SolCnftFulfillBuyArgs)] +pub struct SolCnftFulfillBuy<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: we will check the owner field that matches the pool owner + #[account(mut)] + pub owner: UncheckedAccount<'info>, + #[account(constraint = owner.key() != cosigner.key() @ MMMErrorCode::InvalidCosigner)] + pub cosigner: Signer<'info>, + #[account( + mut, + constraint = verify_referral(&pool, &referral) @ MMMErrorCode::InvalidReferral, + )] + /// CHECK: use verify_referral to check the referral account + pub referral: UncheckedAccount<'info>, + #[account( + mut, + seeds = [POOL_PREFIX.as_bytes(), owner.key().as_ref(), pool.uuid.as_ref()], + has_one = owner @ MMMErrorCode::InvalidOwner, + has_one = cosigner @ MMMErrorCode::InvalidCosigner, + constraint = pool.payment_mint.eq(&Pubkey::default()) @ MMMErrorCode::InvalidPaymentMint, + constraint = pool.expiry == 0 || pool.expiry > Clock::get().unwrap().unix_timestamp @ MMMErrorCode::Expired, + bump + )] + pub pool: Box>, + /// CHECK: it's a pda, and the private key is owned by the seeds + #[account( + mut, + seeds = [BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), pool.key().as_ref()], + bump, + )] + pub buyside_sol_escrow_account: UncheckedAccount<'info>, + + // ==== cNFT transfer args ==== // + #[account( + mut, + seeds = [merkle_tree.key().as_ref()], + seeds::program = bubblegum_program.key(), + bump, + )] + /// CHECK: This account is neither written to nor read from. + pub tree_authority: Account<'info, TreeConfigAnchor>, + + // The account that contains the Merkle tree, initialized by create_tree. + /// CHECK: This account is modified in the downstream Bubblegum program + #[account(mut)] + merkle_tree: UncheckedAccount<'info>, + // Used by bubblegum for logging (CPI) + #[account(address = Pubkey::from_str("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV").unwrap())] + log_wrapper: Option>, + + bubblegum_program: Program<'info, BubblegumProgram>, + + // The Solana Program Library spl-account-compression program ID. + #[account(address = Pubkey::from_str("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK").unwrap())] + compression_program: Option>, + + #[account( + init_if_needed, + payer = payer, + seeds = [ + SELL_STATE_PREFIX.as_bytes(), + pool.key().as_ref(), + merkle_tree.key().as_ref(), + args.index.to_le_bytes().as_ref(), + ], + space = SellState::LEN, + bump + )] + pub sell_state: Account<'info, SellState>, + /// CHECK: will be used for allowlist checks + pub allowlist_aux_account: UncheckedAccount<'info>, + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, + // Remaining accounts + // Branch: using shared escrow accounts + // 0: m2_program + // 1: shared_escrow_account + // 2+: creator accounts + // Branch: not using shared escrow accounts + // 0+: creator accounts +} + +pub fn handler<'info>( + ctx: Context<'_, '_, '_, 'info, SolCnftFulfillBuy<'info>>, + args: SolCnftFulfillBuyArgs, +) -> Result<()> { + // let payer = &ctx.accounts.payer; + let owner = &ctx.accounts.owner; + let pool = &mut ctx.accounts.pool; + // let sell_state = &mut ctx.accounts.sell_state; + // let merkle_tree = &ctx.accounts.merkle_tree; + + if pool.using_shared_escrow() { + return Err(MMMErrorCode::InvalidAccountState.into()); + } + + log_pool("post_sol_cnft_fulfill_buy", pool)?; + try_close_pool(pool, owner.to_account_info())?; + + Ok(()) +} \ No newline at end of file diff --git a/programs/mmm/src/instructions/mod.rs b/programs/mmm/src/instructions/mod.rs index a12cc04..f6157d7 100644 --- a/programs/mmm/src/instructions/mod.rs +++ b/programs/mmm/src/instructions/mod.rs @@ -6,6 +6,7 @@ pub mod mip1; pub mod mpl_core_asset; pub mod ocp; pub mod vanilla; +pub mod cnft; pub use admin::*; pub use ext_vanilla::*; @@ -13,6 +14,7 @@ pub use mip1::*; pub use mpl_core_asset::*; pub use ocp::*; pub use vanilla::*; +pub use cnft::*; use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; diff --git a/programs/mmm/src/lib.rs b/programs/mmm/src/lib.rs index f750238..4731b40 100644 --- a/programs/mmm/src/lib.rs +++ b/programs/mmm/src/lib.rs @@ -181,4 +181,11 @@ pub mod mmm { ) -> Result<()> { instructions::sol_mpl_core_fulfill_buy::handler(ctx, args) } + + pub fn cnft_fulfill_buy<'info>( + ctx: Context<'_, '_, '_, 'info, SolCnftFulfillBuy<'info>>, + args: SolCnftFulfillBuyArgs, + ) -> Result<()> { + instructions::sol_cnft_fulfill_buy::handler(ctx, args) + } } diff --git a/programs/mmm/src/state.rs b/programs/mmm/src/state.rs index 18e901a..1be05ee 100644 --- a/programs/mmm/src/state.rs +++ b/programs/mmm/src/state.rs @@ -1,4 +1,7 @@ +use std::ops::Deref; + use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; +use mpl_bubblegum::accounts::TreeConfig; use crate::constants::*; @@ -134,3 +137,42 @@ impl SellState { 32 + // [u8; 32] 200; // padding } + + +// Wrapper structs to replace the Anchor program types until the Metaplex libs have +// better Anchor support. +pub struct BubblegumProgram; + +impl Id for BubblegumProgram { + fn id() -> Pubkey { + mpl_bubblegum::ID + } +} + + +#[derive(Clone)] +pub struct TreeConfigAnchor(pub TreeConfig); + +impl AccountDeserialize for TreeConfigAnchor { + fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result { + Ok(Self(TreeConfig::from_bytes(buf)?)) + } +} + +impl anchor_lang::Owner for TreeConfigAnchor { + fn owner() -> Pubkey { + // pub use spl_token::ID is used at the top of the file + mpl_bubblegum::ID + } +} + +// No-op since we can't write data to a foreign program's account. +impl AccountSerialize for TreeConfigAnchor {} + +impl Deref for TreeConfigAnchor { + type Target = TreeConfig; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index ae6d289..b24d348 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2155,6 +2155,96 @@ export type Mmm = { } } ] + }, + { + "name": "cnftFulfillBuy", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "treeAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "merkleTree", + "isMut": true, + "isSigner": false + }, + { + "name": "logWrapper", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "bubblegumProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "compressionProgram", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolCnftFulfillBuyArgs" + } + } + ] } ], "accounts": [ @@ -2453,6 +2543,79 @@ export type Mmm = { ] } }, + { + "name": "SolCnftFulfillBuyArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "metadataHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "creatorHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "index", + "type": "u32" + }, + { + "name": "buyerPrice", + "type": "u64" + }, + { + "name": "paymentMint", + "type": "publicKey" + }, + { + "name": "assetAmount", + "type": "u64" + }, + { + "name": "minPaymentAmount", + "type": "u64" + }, + { + "name": "allowlistAux", + "type": { + "option": "string" + } + }, + { + "name": "makerFeeBp", + "type": "i16" + }, + { + "name": "takerFeeBp", + "type": "i16" + } + ] + } + }, { "name": "SolMip1FulfillSellArgs", "type": { @@ -5092,6 +5255,96 @@ export const IDL: Mmm = { } } ] + }, + { + "name": "cnftFulfillBuy", + "accounts": [ + { + "name": "payer", + "isMut": true, + "isSigner": true + }, + { + "name": "owner", + "isMut": true, + "isSigner": false + }, + { + "name": "cosigner", + "isMut": false, + "isSigner": true + }, + { + "name": "referral", + "isMut": true, + "isSigner": false + }, + { + "name": "pool", + "isMut": true, + "isSigner": false + }, + { + "name": "buysideSolEscrowAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "treeAuthority", + "isMut": true, + "isSigner": false + }, + { + "name": "merkleTree", + "isMut": true, + "isSigner": false + }, + { + "name": "logWrapper", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "bubblegumProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "compressionProgram", + "isMut": false, + "isSigner": false, + "isOptional": true + }, + { + "name": "sellState", + "isMut": true, + "isSigner": false + }, + { + "name": "allowlistAuxAccount", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "rent", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "args", + "type": { + "defined": "SolCnftFulfillBuyArgs" + } + } + ] } ], "accounts": [ @@ -5390,6 +5643,79 @@ export const IDL: Mmm = { ] } }, + { + "name": "SolCnftFulfillBuyArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "metadataHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "creatorHash", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "nonce", + "type": "u64" + }, + { + "name": "index", + "type": "u32" + }, + { + "name": "buyerPrice", + "type": "u64" + }, + { + "name": "paymentMint", + "type": "publicKey" + }, + { + "name": "assetAmount", + "type": "u64" + }, + { + "name": "minPaymentAmount", + "type": "u64" + }, + { + "name": "allowlistAux", + "type": { + "option": "string" + } + }, + { + "name": "makerFeeBp", + "type": "i16" + }, + { + "name": "takerFeeBp", + "type": "i16" + } + ] + } + }, { "name": "SolMip1FulfillSellArgs", "type": { From d3623c0ab459aa03c437e124ce39f8ff0c559caa Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 23 Oct 2024 17:32:40 -0700 Subject: [PATCH 06/35] add cnft transfer to fulfill buy --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 32 ++++-- programs/mmm/src/util.rs | 101 +++++++++++++++++- 2 files changed, 126 insertions(+), 7 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index b2531e5..7f95441 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -6,7 +6,7 @@ use crate::{ constants::*, errors::MMMErrorCode, state::{BubblegumProgram, Pool, SellState, TreeConfigAnchor}, - util::{log_pool, try_close_pool}, + util::{log_pool, transfer_compressed_nft, try_close_pool}, verify_referral::verify_referral, }; @@ -88,15 +88,15 @@ pub struct SolCnftFulfillBuy<'info> { /// CHECK: This account is modified in the downstream Bubblegum program #[account(mut)] merkle_tree: UncheckedAccount<'info>, - // Used by bubblegum for logging (CPI) + /// CHECK: Used by bubblegum for logging (CPI) #[account(address = Pubkey::from_str("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV").unwrap())] - log_wrapper: Option>, + log_wrapper: UncheckedAccount<'info>, bubblegum_program: Program<'info, BubblegumProgram>, - // The Solana Program Library spl-account-compression program ID. + /// CHECK: The Solana Program Library spl-account-compression program ID. #[account(address = Pubkey::from_str("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK").unwrap())] - compression_program: Option>, + compression_program: UncheckedAccount<'info>, #[account( init_if_needed, @@ -138,8 +138,28 @@ pub fn handler<'info>( return Err(MMMErrorCode::InvalidAccountState.into()); } + // Transfer CNFT from seller(payer) to buyer (pool owner) + transfer_compressed_nft( + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.owner.to_account_info(), + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + &ctx.accounts.system_program, // Pass as Program without calling to_account_info() + &ctx.remaining_accounts, // TODO: need to extract the the proofs from the remaining accounts + ctx.accounts.bubblegum_program.key(), + args.root, + args.metadata_hash, + args.creator_hash, + args.nonce, + args.index, + None, // signer passed through from ctx + )?; + log_pool("post_sol_cnft_fulfill_buy", pool)?; try_close_pool(pool, owner.to_account_info())?; Ok(()) -} \ No newline at end of file +} diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 2603928..71d10a3 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -20,7 +20,7 @@ use mpl_token_metadata::{ types::{Creator, TokenStandard}, }; use open_creator_protocol::state::Policy; -use solana_program::program::invoke_signed; +use solana_program::{keccak, program::invoke_signed}; use spl_token_2022::{ extension::{ group_member_pointer::GroupMemberPointer, metadata_pointer::MetadataPointer, @@ -1124,6 +1124,105 @@ pub fn create_core_metadata_core(royalties: &Royalties) -> MplCoreMetadata { } } +#[allow(clippy::too_many_arguments)] +pub fn transfer_compressed_nft<'info>( + tree_authority: &AccountInfo<'info>, + leaf_owner: &AccountInfo<'info>, + leaf_delegate: &AccountInfo<'info>, + new_leaf_owner: &AccountInfo<'info>, + merkle_tree: &AccountInfo<'info>, + log_wrapper: &AccountInfo<'info>, + compression_program: &AccountInfo<'info>, + system_program: &Program<'info, System>, + proof_path: &[AccountInfo<'info>], + bubblegum_program_key: Pubkey, + root: [u8; 32], + data_hash: [u8; 32], + creator_hash: [u8; 32], + nonce: u64, + index: u32, + signer_seeds: Option<&[&[u8]]>, +) -> Result<()> { + // proof_path are the accounts that make up the required proof + let proof_path_len = proof_path.len(); + let mut accounts = Vec::with_capacity( + 8 // space for the 8 AccountMetas that are always included (below) + + proof_path_len, + ); + accounts.extend(vec![ + AccountMeta::new_readonly(tree_authority.key(), false), + AccountMeta::new_readonly(leaf_owner.key(), true), + AccountMeta::new_readonly(leaf_delegate.key(), false), + AccountMeta::new_readonly(new_leaf_owner.key(), false), + AccountMeta::new(merkle_tree.key(), false), + AccountMeta::new_readonly(log_wrapper.key(), false), + AccountMeta::new_readonly(compression_program.key(), false), + AccountMeta::new_readonly(system_program.key(), false), + ]); + + let transfer_discriminator: [u8; 8] = [163, 52, 200, 231, 140, 3, 69, 186]; + + let mut data = Vec::with_capacity( + 8 // The length of transfer_discriminator, + + root.len() + + data_hash.len() + + creator_hash.len() + + 8 // The length of the nonce + + 8, // The length of the index + ); + data.extend(transfer_discriminator); + data.extend(root); + data.extend(data_hash); + data.extend(creator_hash); + data.extend(nonce.to_le_bytes()); + data.extend(index.to_le_bytes()); + + let mut account_infos = Vec::with_capacity( + 8 // space for the 8 AccountInfos that are always included (below) + + proof_path_len, + ); + account_infos.extend(vec![ + tree_authority.to_account_info(), + leaf_owner.to_account_info(), + leaf_delegate.to_account_info(), + new_leaf_owner.to_account_info(), + merkle_tree.to_account_info(), + log_wrapper.to_account_info(), + compression_program.to_account_info(), + system_program.to_account_info(), + ]); + + // Add "accounts" (hashes) that make up the merkle proof from the remaining accounts. + for acc in proof_path.iter() { + accounts.push(AccountMeta::new_readonly(acc.key(), false)); + account_infos.push(acc.to_account_info()); + } + + let instruction = solana_program::instruction::Instruction { + program_id: bubblegum_program_key, + accounts, + data, + }; + + match signer_seeds { + Some(seeds) => { + let seeds_array: &[&[&[u8]]] = &[seeds]; + solana_program::program::invoke_signed(&instruction, &account_infos[..], seeds_array) + } + None => solana_program::program::invoke(&instruction, &account_infos[..]), + }?; + Ok(()) +} + +// Taken from Bubblegum's hash_metadata: hashes seller_fee_basis_points to the final data_hash that Bubblegum expects. +// This way we can use the seller_fee_basis_points while still guaranteeing validity. +pub fn hash_metadata_data( + metadata_args_hash: [u8; 32], + seller_fee_basis_points: u16, +) -> Result<[u8; 32]> { + Ok(keccak::hashv(&[&metadata_args_hash, &seller_fee_basis_points.to_le_bytes()]).to_bytes()) +} + #[cfg(test)] mod tests { use anchor_spl::token_2022; From 4aad3a22d288a16f8e2e1bc4c44bb9fcc5a1bb55 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 23 Oct 2024 18:14:55 -0700 Subject: [PATCH 07/35] add collection offer creation in test --- sdk/src/cnft.ts | 7 - sdk/src/idl/mmm.ts | 12 +- tests/mmm-cnft.spec.ts | 97 ++++++- tests/utils/cnft.ts | 613 +++++++++++++++++++++-------------------- 4 files changed, 409 insertions(+), 320 deletions(-) diff --git a/sdk/src/cnft.ts b/sdk/src/cnft.ts index 787c502..13acd4e 100644 --- a/sdk/src/cnft.ts +++ b/sdk/src/cnft.ts @@ -29,10 +29,3 @@ export interface BubblegumNftArgs { tree: BubblegumTreeRef; nft: CNFT; } - -export interface CnftDepositSellArgs { - nft: BubblegumNftArgs; - seller: PublicKey; - nftDelegate: PublicKey; // delegate for the NFT, required to be correctly for cNFT transfers - price: number; -} \ No newline at end of file diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index b24d348..fa4e806 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2202,8 +2202,7 @@ export type Mmm = { { "name": "logWrapper", "isMut": false, - "isSigner": false, - "isOptional": true + "isSigner": false }, { "name": "bubblegumProgram", @@ -2213,8 +2212,7 @@ export type Mmm = { { "name": "compressionProgram", "isMut": false, - "isSigner": false, - "isOptional": true + "isSigner": false }, { "name": "sellState", @@ -5302,8 +5300,7 @@ export const IDL: Mmm = { { "name": "logWrapper", "isMut": false, - "isSigner": false, - "isOptional": true + "isSigner": false }, { "name": "bubblegumProgram", @@ -5313,8 +5310,7 @@ export const IDL: Mmm = { { "name": "compressionProgram", "isMut": false, - "isSigner": false, - "isOptional": true + "isSigner": false }, { "name": "sellState", diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 7fe040a..3fea06e 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -1,27 +1,105 @@ import * as anchor from '@project-serum/anchor'; import { generateSigner, sol, Umi } from '@metaplex-foundation/umi'; import { + airdrop, + createPool, createUmi, getPubKey, - setupTreeAndListing, + setupTree, verifyOwnership, } from './utils'; +import { + getM2BuyerSharedEscrow, + getMMMBuysideSolEscrowPDA, + IDL, + Mmm, + MMMProgramID, +} from '../sdk/src'; +import { + Keypair, + LAMPORTS_PER_SOL, + PublicKey, + SystemProgram, +} from '@solana/web3.js'; + +async function createCNftCollectionOffer( + program: anchor.Program, + poolArgs: Parameters[1], + sharedEscrow?: boolean, + sharedEscrowCount?: number, +) { + const poolData = await createPool(program, { + ...poolArgs, + }); + + const poolKey = poolData.poolKey; + const { key: buysideSolEscrowAccount } = getMMMBuysideSolEscrowPDA( + program.programId, + poolData.poolKey, + ); + + await program.methods + .solDepositBuy({ paymentAmount: new anchor.BN(10 * LAMPORTS_PER_SOL) }) + .accountsStrict({ + owner: poolArgs.owner, + cosigner: poolArgs.cosigner?.publicKey ?? poolArgs.owner, + pool: poolKey, + buysideSolEscrowAccount, + systemProgram: SystemProgram.programId, + }) + .signers([...(poolArgs.cosigner ? [poolArgs.cosigner] : [])]) + .rpc({ skipPreflight: true }); + + if (sharedEscrow) { + const sharedEscrowAccount = getM2BuyerSharedEscrow(poolArgs.owner).key; + await program.methods + .setSharedEscrow({ + sharedEscrowCount: new anchor.BN(sharedEscrowCount || 2), + }) + .accountsStrict({ + owner: poolArgs.owner, + cosigner: poolArgs.cosigner?.publicKey ?? poolArgs.owner, + pool: poolKey, + sharedEscrowAccount, + }) + .signers([...(poolArgs.cosigner ? [poolArgs.cosigner] : [])]) + .rpc(); + } + + return { + buysideSolEscrowAccount, + poolData, + }; +} describe('cnft tests', () => { const endpoint = 'http://localhost:8899'; - const conn = new anchor.web3.Connection(endpoint, 'processed'); + const buyer = new anchor.Wallet(Keypair.generate()); + const connection = new anchor.web3.Connection(endpoint, 'processed'); + const provider = new anchor.AnchorProvider(connection, buyer, { + commitment: 'processed', + }); + let umi: Umi; + const program = new anchor.Program( + IDL, + MMMProgramID, + provider, + ) as anchor.Program; + const cosigner = Keypair.generate(); beforeAll(async () => { umi = await createUmi(endpoint, sol(3)); + airdrop(connection, buyer.publicKey, 100); + airdrop(connection, cosigner.publicKey, 100); }); it.only('cnft fulfill buy', async () => { const umi = await createUmi(endpoint, sol(3)); const seller = generateSigner(umi); await umi.rpc.airdrop(seller.publicKey, sol(1)); - await umi.rpc.airdrop(seller.publicKey, sol(10)); + // 1. Create a tree. const { merkleTree, escrowedProof, @@ -29,12 +107,23 @@ describe('cnft tests', () => { metadata, getBubblegumTreeRef, getCnftRef, - } = await setupTreeAndListing(umi, seller); + baseFulfillBuyArgs, + } = await setupTree(umi, seller); const merkleyTreePubkey = getPubKey(merkleTree); console.log('merkleyTreePubkey', merkleyTreePubkey.toBase58()); + // 2. Create an offer. + console.log(`buyer: ${buyer.publicKey.toBase58()}`); + const { buysideSolEscrowAccount, poolData } = + await createCNftCollectionOffer(program, { + owner: new PublicKey(buyer.publicKey), + cosigner, + }); + + console.log(`poolData: ${JSON.stringify(poolData)}`); + // Verify that buyer now owns the cNFT. await verifyOwnership( umi, diff --git a/tests/utils/cnft.ts b/tests/utils/cnft.ts index e0f3a2d..88fa0f0 100644 --- a/tests/utils/cnft.ts +++ b/tests/utils/cnft.ts @@ -1,307 +1,318 @@ import { - Creator, - MetadataArgsArgs, - mplBubblegum, - createTree as baseCreateTree, - mintV1 as baseMintV1, - fetchMerkleTree, - findLeafAssetIdPda, - hashLeaf, - hashMetadataCreators, - verifyCreator, - getCurrentRoot, - hashMetadataData, - hash, - getMetadataArgsSerializer, - getMerkleProof, - verifyLeaf, - } from '@metaplex-foundation/mpl-bubblegum'; - import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; - import { - Context, - generateSigner, - KeypairSigner, - none, - Pda, - PublicKey, - publicKey, - sol, - SolAmount, - Umi, - PublicKey as UmiPublicKey, - } from '@metaplex-foundation/umi'; - import { createUmi as baseCreateUmi } from '@metaplex-foundation/umi-bundle-tests'; - import { BubblegumTreeRef, CNFT, CnftDepositSellArgs } from '../../sdk/src'; - import { PublicKey as Web3PubKey } from '@solana/web3.js'; - - export const ME_TREASURY = new Web3PubKey( - 'rFqFJ9g7TGBD8Ed7TPDnvGKZ5pWLPDyxLcvcH2eRCtt', + Creator, + MetadataArgsArgs, + mplBubblegum, + createTree as baseCreateTree, + mintV1 as baseMintV1, + fetchMerkleTree, + findLeafAssetIdPda, + hashLeaf, + hashMetadataCreators, + verifyCreator, + getCurrentRoot, + hashMetadataData, + hash, + getMetadataArgsSerializer, + getMerkleProof, + verifyLeaf, +} from '@metaplex-foundation/mpl-bubblegum'; +import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; +import { + Context, + generateSigner, + KeypairSigner, + none, + Pda, + PublicKey, + publicKey, + sol, + SolAmount, + Umi, + PublicKey as UmiPublicKey, +} from '@metaplex-foundation/umi'; +import { createUmi as baseCreateUmi } from '@metaplex-foundation/umi-bundle-tests'; +import { BubblegumTreeRef, CNFT } from '../../sdk/src'; +import { PublicKey as Web3PubKey } from '@solana/web3.js'; + +export const ME_TREASURY = new Web3PubKey( + 'rFqFJ9g7TGBD8Ed7TPDnvGKZ5pWLPDyxLcvcH2eRCtt', +); + +export const treasury = publicKey(ME_TREASURY.toBase58()); + +export const createUmi = async (endpoint?: string, airdropAmount?: SolAmount) => + (await baseCreateUmi(endpoint, undefined, airdropAmount)) + .use(mplTokenMetadata()) + .use(mplBubblegum()); + +export const createTree = async ( + context: Context, + input: Partial[1]> = {}, +): Promise => { + const merkleTree = generateSigner(context); + const builder = await baseCreateTree(context, { + merkleTree, + maxDepth: 14, + maxBufferSize: 64, + ...input, + }); + await builder.sendAndConfirm(context); + return merkleTree.publicKey; +}; + +export async function getCreatorPair(umi: Umi): Promise { + const creator1 = generateSigner(umi); + const creator2 = generateSigner(umi); + await umi.rpc.airdrop(creator1.publicKey, sol(1)); + await umi.rpc.airdrop(creator2.publicKey, sol(1)); + return [creator1, creator2]; +} + +export async function initUnverifiedCreatorsArray( + creators: KeypairSigner[], +): Promise { + return [ + { + address: creators[0].publicKey, + verified: false, + share: 60, + }, + { + address: creators[1].publicKey, + verified: false, + share: 40, + }, + ]; +} + +export const mint = async ( + context: Context, + input: Omit[1], 'metadata' | 'leafOwner'> & { + leafIndex?: number | bigint; + metadata?: Partial[1]['metadata']>; + leafOwner?: PublicKey; + creators?: Parameters[1]['metadata']['creators']; + }, +): Promise<{ + metadata: MetadataArgsArgs; + assetId: Pda; + leaf: PublicKey; + leafIndex: number; + creatorsHash: PublicKey; +}> => { + const merkleTree = publicKey(input.merkleTree, false); + const leafOwner = input.leafOwner ?? context.identity.publicKey; + const leafIndex = Number( + input.leafIndex ?? + (await fetchMerkleTree(context, merkleTree)).tree.activeIndex, ); - - export const treasury = publicKey(ME_TREASURY.toBase58()); - - export const createUmi = async (endpoint?: string, airdropAmount?: SolAmount) => - (await baseCreateUmi(endpoint, undefined, airdropAmount)) - .use(mplTokenMetadata()) - .use(mplBubblegum()); - - export const createTree = async ( - context: Context, - input: Partial[1]> = {}, - ): Promise => { - const merkleTree = generateSigner(context); - const builder = await baseCreateTree(context, { - merkleTree, - maxDepth: 14, - maxBufferSize: 64, - ...input, - }); - await builder.sendAndConfirm(context); - return merkleTree.publicKey; + const leafCreators = input.creators ?? []; + const metadata: MetadataArgsArgs = { + name: 'My NFT', + uri: 'https://example.com/my-nft.json', + sellerFeeBasisPoints: 500, // 5% + collection: none(), + creators: leafCreators, + ...input.metadata, }; - - export async function getCreatorPair(umi: Umi): Promise { - const creator1 = generateSigner(umi); - const creator2 = generateSigner(umi); - await umi.rpc.airdrop(creator1.publicKey, sol(1)); - await umi.rpc.airdrop(creator2.publicKey, sol(1)); - return [creator1, creator2]; - } - - export async function initUnverifiedCreatorsArray( - creators: KeypairSigner[], - ): Promise { - return [ - { - address: creators[0].publicKey, - verified: false, - share: 60, - }, - { - address: creators[1].publicKey, - verified: false, - share: 40, - }, - ]; - } - - export const mint = async ( - context: Context, - input: Omit[1], 'metadata' | 'leafOwner'> & { - leafIndex?: number | bigint; - metadata?: Partial[1]['metadata']>; - leafOwner?: PublicKey; - creators?: Parameters[1]['metadata']['creators']; - }, - ): Promise<{ - metadata: MetadataArgsArgs; - assetId: Pda; - leaf: PublicKey; - leafIndex: number; - creatorsHash: PublicKey; - }> => { - const merkleTree = publicKey(input.merkleTree, false); - const leafOwner = input.leafOwner ?? context.identity.publicKey; - const leafIndex = Number( - input.leafIndex ?? - (await fetchMerkleTree(context, merkleTree)).tree.activeIndex, - ); - const leafCreators = input.creators ?? []; - const metadata: MetadataArgsArgs = { - name: 'My NFT', - uri: 'https://example.com/my-nft.json', - sellerFeeBasisPoints: 500, // 5% - collection: none(), - creators: leafCreators, - ...input.metadata, - }; - - await baseMintV1(context, { - ...input, - metadata, - leafOwner, - }).sendAndConfirm(context); - - return { - metadata, - assetId: findLeafAssetIdPda(context, { merkleTree, leafIndex }), - leafIndex, - leaf: publicKey( - hashLeaf(context, { - merkleTree, - owner: publicKey(leafOwner, false), - delegate: publicKey(input.leafDelegate ?? leafOwner, false), - leafIndex, - metadata, - }), - ), - creatorsHash: publicKey(hashMetadataCreators(leafCreators)), - }; + + await baseMintV1(context, { + ...input, + metadata, + leafOwner, + }).sendAndConfirm(context); + + return { + metadata, + assetId: findLeafAssetIdPda(context, { merkleTree, leafIndex }), + leafIndex, + leaf: publicKey( + hashLeaf(context, { + merkleTree, + owner: publicKey(leafOwner, false), + delegate: publicKey(input.leafDelegate ?? leafOwner, false), + leafIndex, + metadata, + }), + ), + creatorsHash: publicKey(hashMetadataCreators(leafCreators)), }; - - // This is Hash(metadataArgs). Useful for verifying sellers fee basis points are valid. - // NOTE: this does not perform any checks on the hash, it is recommended to use getMetadataHashChecked - // in production!! - export function hashMetadataArgsArgs(metadata: MetadataArgsArgs): Uint8Array { - return hash(getMetadataArgsSerializer().serialize(metadata)); - } - - export function bufferToArray(buffer: Buffer): number[] { - const nums: number[] = []; - for (let i = 0; i < buffer.length; i++) { - nums.push(buffer[i]); - } - return nums; - } - - export function getPubKey(umiKey: UmiPublicKey) { - return new Web3PubKey(umiKey.toString()); +}; + +// This is Hash(metadataArgs). Useful for verifying sellers fee basis points are valid. +// NOTE: this does not perform any checks on the hash, it is recommended to use getMetadataHashChecked +// in production!! +export function hashMetadataArgsArgs(metadata: MetadataArgsArgs): Uint8Array { + return hash(getMetadataArgsSerializer().serialize(metadata)); +} + +export function bufferToArray(buffer: Buffer): number[] { + const nums: number[] = []; + for (let i = 0; i < buffer.length; i++) { + nums.push(buffer[i]); } - - export async function verifyOwnership( - umi: Umi, - merkleTree: UmiPublicKey, - expectedOwner: UmiPublicKey, - leafIndex: number, - metadata: MetadataArgsArgs, - preMints: { leaf: UmiPublicKey }[], - ): Promise<{ currentProof: UmiPublicKey[] }> { - const escrowedLeaf = hashLeaf(umi, { - merkleTree, - owner: expectedOwner, - leafIndex, - metadata, - }); - - const currentProof = getMerkleProof( - [...preMints.map((m) => m.leaf), publicKey(escrowedLeaf)], - 5, - publicKey(escrowedLeaf), - ); - - const merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); - - await verifyLeaf(umi, { - merkleTree, - root: getCurrentRoot(merkleTreeAccount.tree), - leaf: escrowedLeaf, - index: leafIndex, - proof: currentProof, - }).sendAndConfirm(umi); - - return { currentProof }; - } - - export async function setupTreeAndListing( - umi: Umi, + return nums; +} + +export function getPubKey(umiKey: UmiPublicKey) { + return new Web3PubKey(umiKey.toString()); +} + +export async function verifyOwnership( + umi: Umi, + merkleTree: UmiPublicKey, + expectedOwner: UmiPublicKey, + leafIndex: number, + metadata: MetadataArgsArgs, + preMints: { leaf: UmiPublicKey }[], +): Promise<{ currentProof: UmiPublicKey[] }> { + const escrowedLeaf = hashLeaf(umi, { + merkleTree, + owner: expectedOwner, + leafIndex, + metadata, + }); + + const currentProof = getMerkleProof( + [...preMints.map((m) => m.leaf), publicKey(escrowedLeaf)], + 5, + publicKey(escrowedLeaf), + ); + + const merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + + await verifyLeaf(umi, { + merkleTree, + root: getCurrentRoot(merkleTreeAccount.tree), + leaf: escrowedLeaf, + index: leafIndex, + proof: currentProof, + }).sendAndConfirm(umi); + + return { currentProof }; +} + +export async function setupTree( + umi: Umi, + seller: KeypairSigner, +): Promise<{ + merkleTree: UmiPublicKey; + leaf: UmiPublicKey; + escrowedProof: UmiPublicKey[]; + creators: Creator[]; + leafIndex: number; + metadata: MetadataArgsArgs; + creatorsHash: UmiPublicKey; + getBubblegumTreeRef: () => Promise; + getCnftRef: (proof: UmiPublicKey[]) => CNFT; + baseFulfillBuyArgs: (seller: KeypairSigner, price?: number) => any; +}> { + const maxDepth = 5; + const merkleTree = await createTree(umi, { + maxDepth, + maxBufferSize: 8, + }); + + const creatorSigners = await getCreatorPair(umi); + const unverifiedCreators = await initUnverifiedCreatorsArray(creatorSigners); + + const { metadata, leaf, leafIndex, creatorsHash } = await mint(umi, { + merkleTree, + leafOwner: seller.publicKey, + creators: unverifiedCreators, + }); + + // Verify creator A + await verifyCreator(umi, { + leafOwner: seller.publicKey, + creator: creatorSigners[0], + merkleTree, + root: getCurrentRoot((await fetchMerkleTree(umi, merkleTree)).tree), + nonce: leafIndex, + index: leafIndex, + metadata, + proof: [], + }).sendAndConfirm(umi); + + const updatedMetadata = { + ...metadata, + creators: [ + { address: creatorSigners[0].publicKey, verified: true, share: 60 }, + { address: creatorSigners[1].publicKey, verified: false, share: 40 }, + ], + }; + const leafDataPostVerification = hashLeaf(umi, { + merkleTree, + owner: seller.publicKey, + leafIndex, + metadata: updatedMetadata, + }); + const updatedLeaf = publicKey(leafDataPostVerification); + // Make sure that the leaf is updated with the verifie creator. + const merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); + expect(merkleTreeAccount.tree.rightMostPath.leaf).toEqual(updatedLeaf); + + const getBubblegumTreeRef = async () => ({ + merkleTree: getPubKey(merkleTree), + root: new Web3PubKey( + getCurrentRoot((await fetchMerkleTree(umi, merkleTree)).tree), + ), + dataHash: new Web3PubKey(hashMetadataData(updatedMetadata)), + metadataHash: bufferToArray( + Buffer.from(hashMetadataArgsArgs(updatedMetadata)), + ), + creatorHash: new Web3PubKey(hashMetadataCreators(updatedMetadata.creators)), + nonce: leafIndex, + }); + + const getCnftRef = (proof: UmiPublicKey[]) => ({ + nftIndex: leafIndex, + proofs: proof.map(getPubKey), + }); + + const proof = getMerkleProof([updatedLeaf], maxDepth, updatedLeaf); + + const baseFulfillBuyArgs = async ( seller: KeypairSigner, - ): Promise<{ - merkleTree: UmiPublicKey; - leaf: UmiPublicKey; - escrowedProof: UmiPublicKey[]; - creators: Creator[]; - leafIndex: number; - metadata: MetadataArgsArgs; - creatorsHash: UmiPublicKey; - getBubblegumTreeRef: () => Promise; - getCnftRef: (proof: UmiPublicKey[]) => CNFT; - }> { - const maxDepth = 5; - const merkleTree = await createTree(umi, { - maxDepth, - maxBufferSize: 8, - }); - - const creatorSigners = await getCreatorPair(umi); - const unverifiedCreators = await initUnverifiedCreatorsArray(creatorSigners); - - const { metadata, leaf, leafIndex, creatorsHash } = await mint(umi, { - merkleTree, - leafOwner: seller.publicKey, - creators: unverifiedCreators, - }); - - // Verify creator A - await verifyCreator(umi, { - leafOwner: seller.publicKey, - creator: creatorSigners[0], - merkleTree, - root: getCurrentRoot((await fetchMerkleTree(umi, merkleTree)).tree), - nonce: leafIndex, - index: leafIndex, - metadata, - proof: [], - }).sendAndConfirm(umi); - - const updatedMetadata = { - ...metadata, - creators: [ - { address: creatorSigners[0].publicKey, verified: true, share: 60 }, - { address: creatorSigners[1].publicKey, verified: false, share: 40 }, - ], - }; - const leafDataPostVerification = hashLeaf(umi, { - merkleTree, - owner: seller.publicKey, - leafIndex, - metadata: updatedMetadata, - }); - const updatedLeaf = publicKey(leafDataPostVerification); - // Make sure that the leaf is updated with the verifie creator. - const merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); - expect(merkleTreeAccount.tree.rightMostPath.leaf).toEqual(updatedLeaf); - - const getBubblegumTreeRef = async () => ({ - merkleTree: getPubKey(merkleTree), - root: new Web3PubKey( - getCurrentRoot((await fetchMerkleTree(umi, merkleTree)).tree), - ), - dataHash: new Web3PubKey(hashMetadataData(updatedMetadata)), - metadataHash: bufferToArray( - Buffer.from(hashMetadataArgsArgs(updatedMetadata)), - ), - creatorHash: new Web3PubKey(hashMetadataCreators(updatedMetadata.creators)), - nonce: leafIndex, - }); - - const getCnftRef = (proof: UmiPublicKey[]) => ({ - nftIndex: leafIndex, - proofs: proof.map(getPubKey), - }); - - const proof = getMerkleProof([updatedLeaf], maxDepth, updatedLeaf); - - const sellArgs: CnftDepositSellArgs = { - nft: { - tree: await getBubblegumTreeRef(), - nft: getCnftRef(proof), - }, - seller: getPubKey(seller.publicKey), - nftDelegate: getPubKey(seller.publicKey), - price: 100, - }; - - // await runSell(umi, m3Client, seller, sellArgs); - - // Verify that M3 program owns the cNFT. - const { currentProof: escrowedProof } = await verifyOwnership( - umi, - merkleTree, - seller.publicKey, // !!!! this need to be changed to the pool? - leafIndex, - updatedMetadata, - [], - ); - - return { - merkleTree, - leaf, - escrowedProof, - leafIndex, - metadata: updatedMetadata, - creatorsHash, - creators: updatedMetadata.creators, - getBubblegumTreeRef, - getCnftRef, - }; - } \ No newline at end of file + price: number = 100, + ) => ({ + nft: { + tree: await getBubblegumTreeRef(), + nft: getCnftRef(escrowedProof), + }, + seller: getPubKey(seller.publicKey), + price, + makerFeeBasisPoints: 0, + takerFeeBasisPoints: 0, + creatorRoyalties: { + creators: updatedMetadata.creators.map((c) => ({ + ...c, + address: getPubKey(c.address), + })), + sellerFeeBasisPoints: 500, // 5% royalty + }, + }); + + // Verify that seller owns the cNFT. + const { currentProof: escrowedProof } = await verifyOwnership( + umi, + merkleTree, + seller.publicKey, + leafIndex, + updatedMetadata, + [], + ); + + return { + merkleTree, + leaf, + escrowedProof, + leafIndex, + metadata: updatedMetadata, + creatorsHash, + creators: updatedMetadata.creators, + getBubblegumTreeRef, + getCnftRef, + baseFulfillBuyArgs, + }; +} From d69ce2de5d64d655cc17957e9aabf187622e0e38 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Fri, 25 Oct 2024 17:22:13 -0700 Subject: [PATCH 08/35] Add basic fulfill buy operation --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 5 +- sdk/src/cnft.ts | 34 ++++++ sdk/src/idl/mmm.ts | 20 ---- tests/mmm-cnft.spec.ts | 102 ++++++++++++++++-- tests/utils/cnft.ts | 57 +++------- 5 files changed, 143 insertions(+), 75 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 7f95441..41f5765 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -111,10 +111,7 @@ pub struct SolCnftFulfillBuy<'info> { bump )] pub sell_state: Account<'info, SellState>, - /// CHECK: will be used for allowlist checks - pub allowlist_aux_account: UncheckedAccount<'info>, pub system_program: Program<'info, System>, - pub rent: Sysvar<'info, Rent>, // Remaining accounts // Branch: using shared escrow accounts // 0: m2_program @@ -133,6 +130,8 @@ pub fn handler<'info>( let pool = &mut ctx.accounts.pool; // let sell_state = &mut ctx.accounts.sell_state; // let merkle_tree = &ctx.accounts.merkle_tree; + let (creator_accounts, proof_path) = ctx.remaining_accounts.split_at(creator_shares_length); + if pool.using_shared_escrow() { return Err(MMMErrorCode::InvalidAccountState.into()); diff --git a/sdk/src/cnft.ts b/sdk/src/cnft.ts index 13acd4e..cede5d5 100644 --- a/sdk/src/cnft.ts +++ b/sdk/src/cnft.ts @@ -1,4 +1,7 @@ +import { MPL_BUBBLEGUM_PROGRAM_ID } from '@metaplex-foundation/mpl-bubblegum'; import { PublicKey } from '@solana/web3.js'; +import { PREFIXES } from './constants'; +import { BN } from '@project-serum/anchor'; export interface CNFT { nftIndex: number; @@ -29,3 +32,34 @@ export interface BubblegumNftArgs { tree: BubblegumTreeRef; nft: CNFT; } + +export function getBubblegumAuthorityPDA( + merkleRollPubKey: PublicKey, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [merkleRollPubKey.toBuffer()], + new PublicKey(MPL_BUBBLEGUM_PROGRAM_ID), + ); +} + +export function getByteArray(key: PublicKey): Array { + return Array.from(key.toBuffer()); +} + +export const getMMMCnftSellStatePDA = ( + programId: PublicKey, + pool: PublicKey, + merkleTree: PublicKey, + index: number, +) => { + const [key, bump] = PublicKey.findProgramAddressSync( + [ + Buffer.from(PREFIXES.SELL_STATE), + pool.toBuffer(), + merkleTree.toBuffer(), + new BN(index).toBuffer('le', 4), + ], + programId, + ); + return { key, bump }; +}; diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index fa4e806..f92064f 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2219,20 +2219,10 @@ export type Mmm = { "isMut": true, "isSigner": false }, - { - "name": "allowlistAuxAccount", - "isMut": false, - "isSigner": false - }, { "name": "systemProgram", "isMut": false, "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false } ], "args": [ @@ -5317,20 +5307,10 @@ export const IDL: Mmm = { "isMut": true, "isSigner": false }, - { - "name": "allowlistAuxAccount", - "isMut": false, - "isSigner": false - }, { "name": "systemProgram", "isMut": false, "isSigner": false - }, - { - "name": "rent", - "isMut": false, - "isSigner": false } ], "args": [ diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 3fea06e..d75715f 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -1,5 +1,5 @@ import * as anchor from '@project-serum/anchor'; -import { generateSigner, sol, Umi } from '@metaplex-foundation/umi'; +import { publicKey, sol, Umi } from '@metaplex-foundation/umi'; import { airdrop, createPool, @@ -9,8 +9,12 @@ import { verifyOwnership, } from './utils'; import { + getBubblegumAuthorityPDA, + getByteArray, getM2BuyerSharedEscrow, getMMMBuysideSolEscrowPDA, + getMMMCnftSellStatePDA, + getSolFulfillBuyPrices, IDL, Mmm, MMMProgramID, @@ -21,6 +25,13 @@ import { PublicKey, SystemProgram, } from '@solana/web3.js'; +import { + findLeafAssetIdPda, + MPL_BUBBLEGUM_PROGRAM_ID, + SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, + SPL_NOOP_PROGRAM_ID, +} from '@metaplex-foundation/mpl-bubblegum'; +import { BN } from '@project-serum/anchor'; async function createCNftCollectionOffer( program: anchor.Program, @@ -72,9 +83,12 @@ async function createCNftCollectionOffer( }; } +const SOL = new PublicKey('So11111111111111111111111111111111111111112'); + describe('cnft tests', () => { const endpoint = 'http://localhost:8899'; const buyer = new anchor.Wallet(Keypair.generate()); + const seller = new anchor.Wallet(Keypair.generate()); const connection = new anchor.web3.Connection(endpoint, 'processed'); const provider = new anchor.AnchorProvider(connection, buyer, { commitment: 'processed', @@ -91,14 +105,12 @@ describe('cnft tests', () => { beforeAll(async () => { umi = await createUmi(endpoint, sol(3)); airdrop(connection, buyer.publicKey, 100); + airdrop(connection, seller.publicKey, 100); airdrop(connection, cosigner.publicKey, 100); }); it.only('cnft fulfill buy', async () => { const umi = await createUmi(endpoint, sol(3)); - const seller = generateSigner(umi); - await umi.rpc.airdrop(seller.publicKey, sol(1)); - // 1. Create a tree. const { merkleTree, @@ -107,28 +119,96 @@ describe('cnft tests', () => { metadata, getBubblegumTreeRef, getCnftRef, - baseFulfillBuyArgs, - } = await setupTree(umi, seller); + nft, + creatorRoyalties, + } = await setupTree(umi, publicKey(seller.publicKey)); const merkleyTreePubkey = getPubKey(merkleTree); - console.log('merkleyTreePubkey', merkleyTreePubkey.toBase58()); - // 2. Create an offer. - console.log(`buyer: ${buyer.publicKey.toBase58()}`); const { buysideSolEscrowAccount, poolData } = await createCNftCollectionOffer(program, { owner: new PublicKey(buyer.publicKey), cosigner, }); - console.log(`poolData: ${JSON.stringify(poolData)}`); + const [treeAuthority, _] = getBubblegumAuthorityPDA( + new PublicKey(nft.tree.merkleTree), + ); + + const [assetId, bump] = findLeafAssetIdPda(umi, { + merkleTree, + leafIndex, + }); + + const { key: sellState } = getMMMCnftSellStatePDA( + program.programId, + poolData.poolKey, + new PublicKey(nft.tree.merkleTree), + nft.nft.nftIndex, + ); + + const spotPrice = 10; + const expectedBuyPrices = getSolFulfillBuyPrices({ + totalPriceLamports: spotPrice * LAMPORTS_PER_SOL, + lpFeeBp: 0, + takerFeeBp: 100, + metadataRoyaltyBp: 500, + buysideCreatorRoyaltyBp: 10_000, + makerFeeBp: 100, + }); + + // TODO: need to add the proof path inputs, current error: + /** + * Message: Transaction simulation failed: Error processing Instruction 0: Program failed to complete. + Logs: + [ + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK invoke [1]", + "Program log: Instruction: VerifyLeaf", + "Program log: Error using concurrent merkle tree: Invalid root recomputed from proof", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK consumed 5737 of 200000 compute units", + "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK failed: Access violation in stack frame 7 at address 0x200007eb0 of size 8" + ]. + Catch the `SendTransactionError` and call `getLogs()` on it for full details. + */ + await program.methods + .cnftFulfillBuy({ + root: getByteArray(nft.tree.root), + metadataHash: getByteArray(nft.tree.dataHash), + creatorHash: getByteArray(nft.tree.creatorHash), + nonce: new BN(nft.tree.nonce), + index: nft.nft.nftIndex, + buyerPrice: new BN(spotPrice * LAMPORTS_PER_SOL), + paymentMint: SOL, + assetAmount: new BN(1), + minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), + allowlistAux: '', + makerFeeBp: 0, + takerFeeBp: 0, + }) + .accountsStrict({ + payer: new PublicKey(seller.publicKey), + owner: buyer.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount, + treeAuthority, + merkleTree: nft.tree.merkleTree, + logWrapper: SPL_NOOP_PROGRAM_ID, + bubblegumProgram: MPL_BUBBLEGUM_PROGRAM_ID, + compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, + sellState, + systemProgram: SystemProgram.programId, + }) + .signers([cosigner, seller.payer]) + .rpc(); // Verify that buyer now owns the cNFT. await verifyOwnership( umi, merkleTree, - seller.publicKey, + publicKey(buyer.publicKey), leafIndex, metadata, [], diff --git a/tests/utils/cnft.ts b/tests/utils/cnft.ts index 88fa0f0..9cea827 100644 --- a/tests/utils/cnft.ts +++ b/tests/utils/cnft.ts @@ -192,21 +192,7 @@ export async function verifyOwnership( return { currentProof }; } -export async function setupTree( - umi: Umi, - seller: KeypairSigner, -): Promise<{ - merkleTree: UmiPublicKey; - leaf: UmiPublicKey; - escrowedProof: UmiPublicKey[]; - creators: Creator[]; - leafIndex: number; - metadata: MetadataArgsArgs; - creatorsHash: UmiPublicKey; - getBubblegumTreeRef: () => Promise; - getCnftRef: (proof: UmiPublicKey[]) => CNFT; - baseFulfillBuyArgs: (seller: KeypairSigner, price?: number) => any; -}> { +export async function setupTree(umi: Umi, seller: PublicKey) { const maxDepth = 5; const merkleTree = await createTree(umi, { maxDepth, @@ -218,13 +204,13 @@ export async function setupTree( const { metadata, leaf, leafIndex, creatorsHash } = await mint(umi, { merkleTree, - leafOwner: seller.publicKey, + leafOwner: seller, creators: unverifiedCreators, }); // Verify creator A await verifyCreator(umi, { - leafOwner: seller.publicKey, + leafOwner: seller, creator: creatorSigners[0], merkleTree, root: getCurrentRoot((await fetchMerkleTree(umi, merkleTree)).tree), @@ -243,7 +229,7 @@ export async function setupTree( }; const leafDataPostVerification = hashLeaf(umi, { merkleTree, - owner: seller.publicKey, + owner: seller, leafIndex, metadata: updatedMetadata, }); @@ -272,32 +258,11 @@ export async function setupTree( const proof = getMerkleProof([updatedLeaf], maxDepth, updatedLeaf); - const baseFulfillBuyArgs = async ( - seller: KeypairSigner, - price: number = 100, - ) => ({ - nft: { - tree: await getBubblegumTreeRef(), - nft: getCnftRef(escrowedProof), - }, - seller: getPubKey(seller.publicKey), - price, - makerFeeBasisPoints: 0, - takerFeeBasisPoints: 0, - creatorRoyalties: { - creators: updatedMetadata.creators.map((c) => ({ - ...c, - address: getPubKey(c.address), - })), - sellerFeeBasisPoints: 500, // 5% royalty - }, - }); - // Verify that seller owns the cNFT. const { currentProof: escrowedProof } = await verifyOwnership( umi, merkleTree, - seller.publicKey, + seller, leafIndex, updatedMetadata, [], @@ -313,6 +278,16 @@ export async function setupTree( creators: updatedMetadata.creators, getBubblegumTreeRef, getCnftRef, - baseFulfillBuyArgs, + nft: { + tree: await getBubblegumTreeRef(), + nft: getCnftRef(escrowedProof), + }, + creatorRoyalties: { + creators: updatedMetadata.creators.map((c) => ({ + ...c, + address: getPubKey(c.address), + })), + sellerFeeBasisPoints: 500, // 5% royalty + }, }; } From 06d7ec9d82cf6b05a4052d064273097ae3f6161f Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Mon, 28 Oct 2024 14:45:37 -0700 Subject: [PATCH 09/35] successful cnft transfer --- package.json | 3 +- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 4 +-- sdk/src/cnft.ts | 17 +++++++++- tests/mmm-cnft.spec.ts | 34 ++++++++++++------- yarn.lock | 12 +++++++ 5 files changed, 53 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 7af82b6..f9eb872 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "@solana/web3.js": "^1.65.0", "borsh": "^0.7.0", "old-mpl-token-metadata": "npm:@metaplex-foundation/mpl-token-metadata@2.12.0", - "@metaplex-foundation/mpl-bubblegum": "^4.2.1" + "@metaplex-foundation/mpl-bubblegum": "^4.2.1", + "@solana/spl-account-compression": "0.1.8" }, "devDependencies": { "@magiceden-oss/mmm": "file:sdk", diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 41f5765..4b005d7 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -27,6 +27,7 @@ pub struct SolCnftFulfillBuyArgs { nonce: u64, // The index of the leaf in the merkle tree. Can be retrieved from off-chain store. index: u32, + // === Contract args === // // Price of the NFT in the payment_mint. buyer_price: u64, @@ -130,8 +131,7 @@ pub fn handler<'info>( let pool = &mut ctx.accounts.pool; // let sell_state = &mut ctx.accounts.sell_state; // let merkle_tree = &ctx.accounts.merkle_tree; - let (creator_accounts, proof_path) = ctx.remaining_accounts.split_at(creator_shares_length); - + // let (creator_accounts, proof_path) = ctx.remaining_accounts.split_at(creator_shares_length); if pool.using_shared_escrow() { return Err(MMMErrorCode::InvalidAccountState.into()); diff --git a/sdk/src/cnft.ts b/sdk/src/cnft.ts index cede5d5..2f88237 100644 --- a/sdk/src/cnft.ts +++ b/sdk/src/cnft.ts @@ -1,5 +1,5 @@ import { MPL_BUBBLEGUM_PROGRAM_ID } from '@metaplex-foundation/mpl-bubblegum'; -import { PublicKey } from '@solana/web3.js'; +import { AccountMeta, PublicKey } from '@solana/web3.js'; import { PREFIXES } from './constants'; import { BN } from '@project-serum/anchor'; @@ -63,3 +63,18 @@ export const getMMMCnftSellStatePDA = ( ); return { key, bump }; }; + +// get "proof path" from asset proof, these are the accounts that need to be passed to the program as remaining accounts +// may also be empty if tree is small enough, and canopy depth is large enough +export function getProofPath( + proofs: PublicKey[], + canopyDepth?: number, +): AccountMeta[] { + return proofs + .map((pubkey: PublicKey) => ({ + pubkey, + isSigner: false, + isWritable: false, + })) + .slice(0, proofs.length - (!!canopyDepth ? canopyDepth : 0)); +} diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index d75715f..4ce4874 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -14,12 +14,14 @@ import { getM2BuyerSharedEscrow, getMMMBuysideSolEscrowPDA, getMMMCnftSellStatePDA, + getProofPath, getSolFulfillBuyPrices, IDL, Mmm, MMMProgramID, } from '../sdk/src'; import { + AccountMeta, Keypair, LAMPORTS_PER_SOL, PublicKey, @@ -32,6 +34,7 @@ import { SPL_NOOP_PROGRAM_ID, } from '@metaplex-foundation/mpl-bubblegum'; import { BN } from '@project-serum/anchor'; +import { ConcurrentMerkleTreeAccount } from '@solana/spl-account-compression'; async function createCNftCollectionOffer( program: anchor.Program, @@ -158,19 +161,20 @@ describe('cnft tests', () => { makerFeeBp: 100, }); - // TODO: need to add the proof path inputs, current error: - /** - * Message: Transaction simulation failed: Error processing Instruction 0: Program failed to complete. - Logs: - [ - "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK invoke [1]", - "Program log: Instruction: VerifyLeaf", - "Program log: Error using concurrent merkle tree: Invalid root recomputed from proof", - "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK consumed 5737 of 200000 compute units", - "Program cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK failed: Access violation in stack frame 7 at address 0x200007eb0 of size 8" - ]. - Catch the `SendTransactionError` and call `getLogs()` on it for full details. - */ + const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress( + connection, + nft.tree.merkleTree, + ); + + console.log(`merkleTree: ${nft.tree.merkleTree}`); + console.log(`proofs: ${nft.nft.proofs}`); + console.log(`canopyDepth: ${treeAccount.getCanopyDepth()}`); + + const proofPath: AccountMeta[] = getProofPath( + nft.nft.proofs, + treeAccount.getCanopyDepth(), + ); + await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), @@ -201,9 +205,13 @@ describe('cnft tests', () => { sellState, systemProgram: SystemProgram.programId, }) + .remainingAccounts([...proofPath]) .signers([cosigner, seller.payer]) .rpc(); + console.log(`seller: ${seller.publicKey}`); + console.log(`buyer: ${buyer.publicKey}`); + console.log(`nft: ${JSON.stringify(nft)}`); // Verify that buyer now owns the cNFT. await verifyOwnership( umi, diff --git a/yarn.lock b/yarn.lock index 58304ff..8e8f07d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1840,6 +1840,18 @@ "@solana/codecs-strings" "2.0.0-rc.1" "@solana/errors" "2.0.0-rc.1" +"@solana/spl-account-compression@0.1.8": + version "0.1.8" + resolved "https://registry.yarnpkg.com/@solana/spl-account-compression/-/spl-account-compression-0.1.8.tgz#0c1fd052befddd90c2e8704b0b685761799d4bae" + integrity sha512-vsvsx358pVFPtyNd8zIZy0lezR0NuvOykQ29Zq+8oto+kHfTXMGXXQ1tKHUYke6XkINIWLFVg/jDi+1D9RYaqQ== + dependencies: + "@metaplex-foundation/beet" "^0.7.1" + "@metaplex-foundation/beet-solana" "^0.4.0" + bn.js "^5.2.1" + borsh "^0.7.0" + js-sha3 "^0.8.0" + typescript-collections "^1.3.3" + "@solana/spl-account-compression@^0.1.4", "@solana/spl-account-compression@^0.1.8": version "0.1.10" resolved "https://registry.yarnpkg.com/@solana/spl-account-compression/-/spl-account-compression-0.1.10.tgz#b3135ce89349d6090832b3b1d89095badd57e969" From c18508a42979879fdb9bda1e0893c87986e6373b Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Fri, 1 Nov 2024 12:15:16 -0700 Subject: [PATCH 10/35] remove metadata rehash --- package.json | 13 +++-- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 18 +++++- sdk/src/idl/mmm.ts | 32 +++++++++++ tests/mmm-cnft.spec.ts | 32 +++++++++-- tests/utils/cnft.ts | 55 ++++++++++++++++--- 5 files changed, 129 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index f9eb872..e099fcc 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "dependencies": { "@magiceden-oss/open_creator_protocol": "^0.3.2", "@metaplex-foundation/js": "^0.19.4", + "@metaplex-foundation/mpl-bubblegum": "^4.2.1", "@metaplex-foundation/mpl-core": "^0.4.7", "@metaplex-foundation/mpl-token-auth-rules": "^2.0.0", "@metaplex-foundation/mpl-token-metadata": "^3.1.2", @@ -14,25 +15,25 @@ "@metaplex-foundation/umi-bundle-tests": "^0.8.2", "@metaplex-foundation/umi-web3js-adapters": "^0.8.2", "@project-serum/anchor": "^0.26.0", + "@solana/spl-account-compression": "0.1.8", "@solana/spl-token": "^0.4.1", "@solana/spl-token-group": "^0.0.1", "@solana/web3.js": "^1.65.0", "borsh": "^0.7.0", - "old-mpl-token-metadata": "npm:@metaplex-foundation/mpl-token-metadata@2.12.0", - "@metaplex-foundation/mpl-bubblegum": "^4.2.1", - "@solana/spl-account-compression": "0.1.8" + "old-mpl-token-metadata": "npm:@metaplex-foundation/mpl-token-metadata@2.12.0" }, "devDependencies": { "@magiceden-oss/mmm": "file:sdk", + "@metaplex-foundation/digital-asset-standard-api": "^1.0.4", "@metaplex-foundation/mpl-migration-validator": "^0.4.1", "@msgpack/msgpack": "^2.8.0", "@types/jest": "29.0.0", "chai": "^4.3.4", "eslint-config-standard-with-typescript": "^21.0.1", "eslint-plugin-prettier": "^4.0.0", - "prettier": "^2.3.2", - "typescript": "^4.4.2", "jest": "29.0.0", - "ts-jest": "^29.0.0" + "prettier": "^2.3.2", + "ts-jest": "^29.0.0", + "typescript": "^4.4.2" } } diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 4b005d7..570e662 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -39,6 +39,12 @@ pub struct SolCnftFulfillBuyArgs { pub allowlist_aux: Option, // TODO: use it for future allowlist_aux pub maker_fee_bp: i16, // will be checked by cosigner pub taker_fee_bp: i16, // will be checked by cosigner + + // === Creator args === // + creator_shares: Vec, + creator_verified: Vec, + // Creator royalties. Validated against the metadata_hash by Bubblegum after hashing with metadata_hash. + seller_fee_basis_points: u16, } #[derive(Accounts)] @@ -131,13 +137,21 @@ pub fn handler<'info>( let pool = &mut ctx.accounts.pool; // let sell_state = &mut ctx.accounts.sell_state; // let merkle_tree = &ctx.accounts.merkle_tree; - // let (creator_accounts, proof_path) = ctx.remaining_accounts.split_at(creator_shares_length); + // Remaining accounts are 1. (Optional) creator addresses and 2. Merkle proof path. + let creator_shares_length = args.creator_shares.len(); + let creator_shares_clone = args.creator_shares.clone(); + let (creator_accounts, proof_path) = ctx.remaining_accounts.split_at(creator_shares_length); if pool.using_shared_escrow() { return Err(MMMErrorCode::InvalidAccountState.into()); } + msg!("seller fee basis points: {}", args.seller_fee_basis_points); + // Create data_hash from metadata_hash + seller_fee_basis_points (secures creator royalties) + // let data_hash = hash_metadata_data(args.metadata_hash, args.seller_fee_basis_points)?; + // Transfer CNFT from seller(payer) to buyer (pool owner) + // TODO: do I need to send to pool instead? transfer_compressed_nft( &ctx.accounts.tree_authority.to_account_info(), &ctx.accounts.payer.to_account_info(), @@ -147,7 +161,7 @@ pub fn handler<'info>( &ctx.accounts.log_wrapper, &ctx.accounts.compression_program, &ctx.accounts.system_program, // Pass as Program without calling to_account_info() - &ctx.remaining_accounts, // TODO: need to extract the the proofs from the remaining accounts + proof_path, ctx.accounts.bubblegum_program.key(), args.root, args.metadata_hash, diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index f92064f..f9d6b9b 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2600,6 +2600,22 @@ export type Mmm = { { "name": "takerFeeBp", "type": "i16" + }, + { + "name": "creatorShares", + "type": { + "vec": "u16" + } + }, + { + "name": "creatorVerified", + "type": { + "vec": "bool" + } + }, + { + "name": "sellerFeeBasisPoints", + "type": "u16" } ] } @@ -5688,6 +5704,22 @@ export const IDL: Mmm = { { "name": "takerFeeBp", "type": "i16" + }, + { + "name": "creatorShares", + "type": { + "vec": "u16" + } + }, + { + "name": "creatorVerified", + "type": { + "vec": "bool" + } + }, + { + "name": "sellerFeeBasisPoints", + "type": "u16" } ] } diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 4ce4874..0525569 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -4,6 +4,7 @@ import { airdrop, createPool, createUmi, + getCreatorRoyaltiesArgs, getPubKey, setupTree, verifyOwnership, @@ -29,6 +30,7 @@ import { } from '@solana/web3.js'; import { findLeafAssetIdPda, + getAssetWithProof, MPL_BUBBLEGUM_PROGRAM_ID, SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID, @@ -92,9 +94,9 @@ describe('cnft tests', () => { const endpoint = 'http://localhost:8899'; const buyer = new anchor.Wallet(Keypair.generate()); const seller = new anchor.Wallet(Keypair.generate()); - const connection = new anchor.web3.Connection(endpoint, 'processed'); + const connection = new anchor.web3.Connection(endpoint, 'confirmed'); const provider = new anchor.AnchorProvider(connection, buyer, { - commitment: 'processed', + commitment: 'confirmed', }); let umi: Umi; @@ -114,10 +116,13 @@ describe('cnft tests', () => { it.only('cnft fulfill buy', async () => { const umi = await createUmi(endpoint, sol(3)); + + console.log(`buyer: ${buyer.publicKey}`); + console.log(`seller: ${seller.publicKey}`); // 1. Create a tree. const { merkleTree, - escrowedProof, + sellerProof, leafIndex, metadata, getBubblegumTreeRef, @@ -144,6 +149,11 @@ describe('cnft tests', () => { leafIndex, }); + // const asset = await umi.rpc.getAsset(assetId); + // console.log(`asset: ${JSON.stringify(asset)}`); + // const assetWithProof = await getAssetWithProof(umi, assetId); + // console.log(`assetWithProof: ${JSON.stringify(assetWithProof)}`); + const { key: sellState } = getMMMCnftSellStatePDA( program.programId, poolData.poolKey, @@ -175,6 +185,15 @@ describe('cnft tests', () => { treeAccount.getCanopyDepth(), ); + console.log(`proofPath: ${JSON.stringify(proofPath)}`); + + const { + accounts: creatorAccounts, + creatorShares, + creatorVerified, + sellerFeeBasisPoints, + } = getCreatorRoyaltiesArgs(creatorRoyalties); + await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), @@ -189,6 +208,9 @@ describe('cnft tests', () => { allowlistAux: '', makerFeeBp: 0, takerFeeBp: 0, + creatorShares, + creatorVerified, + sellerFeeBasisPoints, }) .accountsStrict({ payer: new PublicKey(seller.publicKey), @@ -205,9 +227,9 @@ describe('cnft tests', () => { sellState, systemProgram: SystemProgram.programId, }) - .remainingAccounts([...proofPath]) + .remainingAccounts([...creatorAccounts, ...proofPath]) .signers([cosigner, seller.payer]) - .rpc(); + .rpc({ skipPreflight: true }); console.log(`seller: ${seller.publicKey}`); console.log(`buyer: ${buyer.publicKey}`); diff --git a/tests/utils/cnft.ts b/tests/utils/cnft.ts index 9cea827..56bc965 100644 --- a/tests/utils/cnft.ts +++ b/tests/utils/cnft.ts @@ -31,8 +31,9 @@ import { PublicKey as UmiPublicKey, } from '@metaplex-foundation/umi'; import { createUmi as baseCreateUmi } from '@metaplex-foundation/umi-bundle-tests'; -import { BubblegumTreeRef, CNFT } from '../../sdk/src'; -import { PublicKey as Web3PubKey } from '@solana/web3.js'; +import { BubblegumTreeRef, CNFT, CreatorRoyaltyConfig } from '../../sdk/src'; +import { AccountMeta, PublicKey as Web3PubKey } from '@solana/web3.js'; +import { dasApi } from '@metaplex-foundation/digital-asset-standard-api'; export const ME_TREASURY = new Web3PubKey( 'rFqFJ9g7TGBD8Ed7TPDnvGKZ5pWLPDyxLcvcH2eRCtt', @@ -41,9 +42,10 @@ export const ME_TREASURY = new Web3PubKey( export const treasury = publicKey(ME_TREASURY.toBase58()); export const createUmi = async (endpoint?: string, airdropAmount?: SolAmount) => - (await baseCreateUmi(endpoint, undefined, airdropAmount)) + (await baseCreateUmi(endpoint, { commitment: 'confirmed' }, airdropAmount)) .use(mplTokenMetadata()) - .use(mplBubblegum()); + .use(mplBubblegum()) + .use(dasApi()); export const createTree = async ( context: Context, @@ -54,6 +56,7 @@ export const createTree = async ( merkleTree, maxDepth: 14, maxBufferSize: 64, + canopyDepth: 3, ...input, }); await builder.sendAndConfirm(context); @@ -202,12 +205,17 @@ export async function setupTree(umi: Umi, seller: PublicKey) { const creatorSigners = await getCreatorPair(umi); const unverifiedCreators = await initUnverifiedCreatorsArray(creatorSigners); - const { metadata, leaf, leafIndex, creatorsHash } = await mint(umi, { + const { metadata, leaf, leafIndex, creatorsHash, assetId } = await mint(umi, { merkleTree, leafOwner: seller, creators: unverifiedCreators, }); + console.log(`merkleTree: ${merkleTree}`); + console.log(`leaf: ${leaf}`); + console.log(`leafIndex: ${leafIndex}`); + console.log(`assetId: ${assetId}`); + // Verify creator A await verifyCreator(umi, { leafOwner: seller, @@ -220,6 +228,7 @@ export async function setupTree(umi: Umi, seller: PublicKey) { proof: [], }).sendAndConfirm(umi); + console.log(`verified creator A`); const updatedMetadata = { ...metadata, creators: [ @@ -259,7 +268,7 @@ export async function setupTree(umi: Umi, seller: PublicKey) { const proof = getMerkleProof([updatedLeaf], maxDepth, updatedLeaf); // Verify that seller owns the cNFT. - const { currentProof: escrowedProof } = await verifyOwnership( + const { currentProof: sellerProof } = await verifyOwnership( umi, merkleTree, seller, @@ -268,10 +277,12 @@ export async function setupTree(umi: Umi, seller: PublicKey) { [], ); + console.log(`sellerProof: ${JSON.stringify(sellerProof)}`); + console.log(`proof: ${JSON.stringify(proof)}`); return { merkleTree, leaf, - escrowedProof, + sellerProof, leafIndex, metadata: updatedMetadata, creatorsHash, @@ -280,7 +291,7 @@ export async function setupTree(umi: Umi, seller: PublicKey) { getCnftRef, nft: { tree: await getBubblegumTreeRef(), - nft: getCnftRef(escrowedProof), + nft: getCnftRef(sellerProof), }, creatorRoyalties: { creators: updatedMetadata.creators.map((c) => ({ @@ -291,3 +302,31 @@ export async function setupTree(umi: Umi, seller: PublicKey) { }, }; } + +export function getCreatorRoyaltiesArgs( + royaltySelection: CreatorRoyaltyConfig, +): { + accounts: AccountMeta[]; + creatorShares: number[]; + creatorVerified: boolean[]; + sellerFeeBasisPoints: number; +} { + const creatorShares: number[] = []; + const creatorVerified: boolean[] = []; + const accounts: AccountMeta[] = royaltySelection.creators.map((creator) => { + creatorShares.push(creator.share); + creatorVerified.push(creator.verified); + return { + pubkey: creator.address, + isSigner: false, + isWritable: true, // so that we can pay creator fees + }; + }); + + return { + accounts, + creatorShares, + creatorVerified, + sellerFeeBasisPoints: royaltySelection.sellerFeeBasisPoints, + }; +} From dfb8b288b82ab3dc56e4bd06f4787349ce0d0569 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Fri, 1 Nov 2024 12:27:30 -0700 Subject: [PATCH 11/35] fix CreatorRoyaltyConfig --- sdk/src/cnft.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/sdk/src/cnft.ts b/sdk/src/cnft.ts index 2f88237..d2ea5d5 100644 --- a/sdk/src/cnft.ts +++ b/sdk/src/cnft.ts @@ -1,7 +1,10 @@ -import { MPL_BUBBLEGUM_PROGRAM_ID } from '@metaplex-foundation/mpl-bubblegum'; +import { + MPL_BUBBLEGUM_PROGRAM_ID, +} from '@metaplex-foundation/mpl-bubblegum'; import { AccountMeta, PublicKey } from '@solana/web3.js'; import { PREFIXES } from './constants'; import { BN } from '@project-serum/anchor'; +import { Creator } from 'old-mpl-token-metadata'; export interface CNFT { nftIndex: number; @@ -78,3 +81,8 @@ export function getProofPath( })) .slice(0, proofs.length - (!!canopyDepth ? canopyDepth : 0)); } + +export interface CreatorRoyaltyConfig { + creators: Creator[]; + sellerFeeBasisPoints: number; +} From 01083858ffe66b3fc8461f274a1d3600ad016d1c Mon Sep 17 00:00:00 2001 From: swimricky Date: Fri, 1 Nov 2024 18:47:04 -0400 Subject: [PATCH 12/35] add using canopyDepth and handling truncated proofs. test passes --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 2 +- sdk/src/cnft.ts | 4 +- tests/mmm-cnft.spec.ts | 106 +++++++++++------- tests/utils/cnft.ts | 101 +++++++++++++---- 4 files changed, 150 insertions(+), 63 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 570e662..acf91e5 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -134,7 +134,7 @@ pub fn handler<'info>( ) -> Result<()> { // let payer = &ctx.accounts.payer; let owner = &ctx.accounts.owner; - let pool = &mut ctx.accounts.pool; + let pool = &ctx.accounts.pool; // let sell_state = &mut ctx.accounts.sell_state; // let merkle_tree = &ctx.accounts.merkle_tree; // Remaining accounts are 1. (Optional) creator addresses and 2. Merkle proof path. diff --git a/sdk/src/cnft.ts b/sdk/src/cnft.ts index d2ea5d5..05b00d5 100644 --- a/sdk/src/cnft.ts +++ b/sdk/src/cnft.ts @@ -1,6 +1,4 @@ -import { - MPL_BUBBLEGUM_PROGRAM_ID, -} from '@metaplex-foundation/mpl-bubblegum'; +import { MPL_BUBBLEGUM_PROGRAM_ID } from '@metaplex-foundation/mpl-bubblegum'; import { AccountMeta, PublicKey } from '@solana/web3.js'; import { PREFIXES } from './constants'; import { BN } from '@project-serum/anchor'; diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 0525569..db1e8fa 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -4,6 +4,7 @@ import { airdrop, createPool, createUmi, + DEFAULT_TEST_SETUP_TREE_PARAMS, getCreatorRoyaltiesArgs, getPubKey, setupTree, @@ -26,6 +27,7 @@ import { Keypair, LAMPORTS_PER_SOL, PublicKey, + SendTransactionError, SystemProgram, } from '@solana/web3.js'; import { @@ -122,14 +124,18 @@ describe('cnft tests', () => { // 1. Create a tree. const { merkleTree, - sellerProof, + sellerProof, //already truncated leafIndex, metadata, getBubblegumTreeRef, getCnftRef, nft, creatorRoyalties, - } = await setupTree(umi, publicKey(seller.publicKey)); + } = await setupTree( + umi, + publicKey(seller.publicKey), + DEFAULT_TEST_SETUP_TREE_PARAMS, + ); const merkleyTreePubkey = getPubKey(merkleTree); @@ -177,13 +183,15 @@ describe('cnft tests', () => { ); console.log(`merkleTree: ${nft.tree.merkleTree}`); - console.log(`proofs: ${nft.nft.proofs}`); + console.log(`proofs: ${nft.nft.fullProof}`); console.log(`canopyDepth: ${treeAccount.getCanopyDepth()}`); const proofPath: AccountMeta[] = getProofPath( - nft.nft.proofs, + nft.nft.fullProof, treeAccount.getCanopyDepth(), ); + console.log(`nft.nft.proofs.length: ${nft.nft.fullProof.length}`); + console.log(`proofPath.length: ${proofPath.length}`); console.log(`proofPath: ${JSON.stringify(proofPath)}`); @@ -193,43 +201,61 @@ describe('cnft tests', () => { creatorVerified, sellerFeeBasisPoints, } = getCreatorRoyaltiesArgs(creatorRoyalties); + console.log(`got creator royalties`); - await program.methods - .cnftFulfillBuy({ - root: getByteArray(nft.tree.root), - metadataHash: getByteArray(nft.tree.dataHash), - creatorHash: getByteArray(nft.tree.creatorHash), - nonce: new BN(nft.tree.nonce), - index: nft.nft.nftIndex, - buyerPrice: new BN(spotPrice * LAMPORTS_PER_SOL), - paymentMint: SOL, - assetAmount: new BN(1), - minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), - allowlistAux: '', - makerFeeBp: 0, - takerFeeBp: 0, - creatorShares, - creatorVerified, - sellerFeeBasisPoints, - }) - .accountsStrict({ - payer: new PublicKey(seller.publicKey), - owner: buyer.publicKey, - cosigner: cosigner.publicKey, - referral: poolData.referral.publicKey, - pool: poolData.poolKey, - buysideSolEscrowAccount, - treeAuthority, - merkleTree: nft.tree.merkleTree, - logWrapper: SPL_NOOP_PROGRAM_ID, - bubblegumProgram: MPL_BUBBLEGUM_PROGRAM_ID, - compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, - sellState, - systemProgram: SystemProgram.programId, - }) - .remainingAccounts([...creatorAccounts, ...proofPath]) - .signers([cosigner, seller.payer]) - .rpc({ skipPreflight: true }); + try { + const fulfillBuyTxnSig = await program.methods + .cnftFulfillBuy({ + root: getByteArray(nft.tree.root), + metadataHash: getByteArray(nft.tree.dataHash), + creatorHash: getByteArray(nft.tree.creatorHash), + nonce: new BN(nft.tree.nonce), + index: nft.nft.nftIndex, + buyerPrice: new BN(spotPrice * LAMPORTS_PER_SOL), + paymentMint: SOL, + assetAmount: new BN(1), + minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), + allowlistAux: '', + makerFeeBp: 0, + takerFeeBp: 0, + creatorShares, + creatorVerified, + sellerFeeBasisPoints, + }) + .accountsStrict({ + payer: new PublicKey(seller.publicKey), + owner: buyer.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount, + treeAuthority, + merkleTree: nft.tree.merkleTree, + logWrapper: SPL_NOOP_PROGRAM_ID, + bubblegumProgram: MPL_BUBBLEGUM_PROGRAM_ID, + compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, + sellState, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts([...creatorAccounts, ...proofPath]) + .signers([cosigner, seller.payer]) + // note: skipPreflight causes some weird error. + // so just surround in this try-catch to get the logs + .rpc(/* { skipPreflight: true } */); + console.log(`fulfillBuyTxnSig: ${fulfillBuyTxnSig}`); + } catch (e) { + if (e instanceof SendTransactionError) { + const err = e as SendTransactionError; + console.log( + `err.logs: ${JSON.stringify( + await err.getLogs(provider.connection), + null, + 2, + )}`, + ); + } + throw e; + } console.log(`seller: ${seller.publicKey}`); console.log(`buyer: ${buyer.publicKey}`); diff --git a/tests/utils/cnft.ts b/tests/utils/cnft.ts index 56bc965..082db9c 100644 --- a/tests/utils/cnft.ts +++ b/tests/utils/cnft.ts @@ -15,6 +15,7 @@ import { getMetadataArgsSerializer, getMerkleProof, verifyLeaf, + MerkleTree, } from '@metaplex-foundation/mpl-bubblegum'; import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; import { @@ -54,10 +55,9 @@ export const createTree = async ( const merkleTree = generateSigner(context); const builder = await baseCreateTree(context, { merkleTree, - maxDepth: 14, - maxBufferSize: 64, - canopyDepth: 3, - ...input, + maxDepth: input.maxDepth ?? 14, + maxBufferSize: input.maxBufferSize ?? 64, + canopyDepth: input.canopyDepth, }); await builder.sendAndConfirm(context); return merkleTree.publicKey; @@ -161,6 +161,16 @@ export function getPubKey(umiKey: UmiPublicKey) { return new Web3PubKey(umiKey.toString()); } +/** + * Verifies that the expectedOwner owns the leaf at the given leafIndex. + * @param umi + * @param merkleTree + * @param expectedOwner + * @param leafIndex + * @param metadata current metadata of the leaf + * @param preMints + * @returns the **truncated** proof used for verification. + */ export async function verifyOwnership( umi: Umi, merkleTree: UmiPublicKey, @@ -175,31 +185,50 @@ export async function verifyOwnership( leafIndex, metadata, }); + const merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); - const currentProof = getMerkleProof( + const currentProof = getTruncatedMerkleProof( + getCanopyDepth(merkleTreeAccount), [...preMints.map((m) => m.leaf), publicKey(escrowedLeaf)], - 5, + merkleTreeAccount.treeHeader.maxDepth, publicKey(escrowedLeaf), ); - const merkleTreeAccount = await fetchMerkleTree(umi, merkleTree); - - await verifyLeaf(umi, { + const { result } = await verifyLeaf(umi, { merkleTree, root: getCurrentRoot(merkleTreeAccount.tree), leaf: escrowedLeaf, index: leafIndex, proof: currentProof, }).sendAndConfirm(umi); + console.log( + `verified ${expectedOwner} owns leaf at index ${leafIndex}. Result: ${JSON.stringify( + result, + )}`, + ); return { currentProof }; } -export async function setupTree(umi: Umi, seller: PublicKey) { - const maxDepth = 5; +export const DEFAULT_TEST_SETUP_TREE_PARAMS = { + maxDepth: 14, + maxBufferSize: 64, + canopyDepth: 9, +}; + +export async function setupTree( + umi: Umi, + seller: PublicKey, + treeParams: { + maxDepth: number; + maxBufferSize: number; + canopyDepth: number; + }, +) { const merkleTree = await createTree(umi, { - maxDepth, - maxBufferSize: 8, + maxDepth: treeParams.maxDepth, + maxBufferSize: treeParams.maxBufferSize, + canopyDepth: treeParams.canopyDepth, }); const creatorSigners = await getCreatorPair(umi); @@ -216,7 +245,14 @@ export async function setupTree(umi: Umi, seller: PublicKey) { console.log(`leafIndex: ${leafIndex}`); console.log(`assetId: ${assetId}`); + const verifyCreatorProofTruncated = getTruncatedMerkleProof( + treeParams.canopyDepth, + [leaf], + treeParams.maxDepth, + leaf, + ); // Verify creator A + await verifyCreator(umi, { leafOwner: seller, creator: creatorSigners[0], @@ -225,7 +261,7 @@ export async function setupTree(umi: Umi, seller: PublicKey) { nonce: leafIndex, index: leafIndex, metadata, - proof: [], + proof: verifyCreatorProofTruncated, }).sendAndConfirm(umi); console.log(`verified creator A`); @@ -262,10 +298,14 @@ export async function setupTree(umi: Umi, seller: PublicKey) { const getCnftRef = (proof: UmiPublicKey[]) => ({ nftIndex: leafIndex, - proofs: proof.map(getPubKey), + fullProof: proof.map(getPubKey), }); - const proof = getMerkleProof([updatedLeaf], maxDepth, updatedLeaf); + const fullProof = getMerkleProof( + [updatedLeaf], + treeParams.maxDepth, + updatedLeaf, + ); // Verify that seller owns the cNFT. const { currentProof: sellerProof } = await verifyOwnership( @@ -277,8 +317,11 @@ export async function setupTree(umi: Umi, seller: PublicKey) { [], ); - console.log(`sellerProof: ${JSON.stringify(sellerProof)}`); - console.log(`proof: ${JSON.stringify(proof)}`); + console.log(` + [setupTree] + fullProof(length: ${fullProof.length}): ${JSON.stringify(fullProof)} + sellerProof[truncated](length: ${sellerProof.length}): ${JSON.stringify(sellerProof)} + `); return { merkleTree, leaf, @@ -291,7 +334,7 @@ export async function setupTree(umi: Umi, seller: PublicKey) { getCnftRef, nft: { tree: await getBubblegumTreeRef(), - nft: getCnftRef(sellerProof), + nft: getCnftRef(fullProof), }, creatorRoyalties: { creators: updatedMetadata.creators.map((c) => ({ @@ -330,3 +373,23 @@ export function getCreatorRoyaltiesArgs( sellerFeeBasisPoints: royaltySelection.sellerFeeBasisPoints, }; } + +export function truncateMerkleProof(proof: PublicKey[], canopyDepth: number) { + return proof.slice(0, canopyDepth === 0 ? undefined : -canopyDepth); +} + +export function getTruncatedMerkleProof( + canopyDepth: number, + leaves: PublicKey[], + maxDepth: number, + leaf: PublicKey, + index?: number | undefined, +) { + const proof = getMerkleProof(leaves, maxDepth, leaf, index); + return truncateMerkleProof(proof, canopyDepth); +} + +// Utility method to calculate the canopy depth from a metaplex MerkleTree type +export function getCanopyDepth(merkleTreeAccount: MerkleTree) { + return Math.log2(merkleTreeAccount.canopy.length + 2) - 1; +} From ad2f360a80099c5fbdf8c89f46263632c3b0cacb Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 6 Nov 2024 17:08:33 -0800 Subject: [PATCH 13/35] pass metadata args in --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 39 +++ sdk/src/idl/mmm.ts | 268 ++++++++++++++++++ tests/mmm-cnft.spec.ts | 35 ++- 3 files changed, 341 insertions(+), 1 deletion(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index acf91e5..2c09fc8 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -10,6 +10,43 @@ use crate::{ verify_referral::verify_referral, }; +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CollectionArgs { + pub verified: bool, + pub key: Pubkey, // Assuming PublicKey is equivalent to Pubkey in Rust +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct UsesArgs { + pub use_method: u8, + pub remaining: u64, // Use u64 for large numbers + pub total: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreatorArgs { + pub address: Pubkey, // Assuming PublicKey is equivalent to Pubkey in Rust + pub verified: bool, + pub share: u8, // Assuming share is a percentage, use u8 +} + +// Define the MetadataArgs struct +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct MetadataArgs { + pub name: String, + pub symbol: String, // Changed from Option to String + pub uri: String, + pub seller_fee_basis_points: u16, + pub primary_sale_happened: bool, // Changed from Option to bool + pub is_mutable: bool, // Changed from Option to bool + pub edition_nonce: Option, + pub token_standard: Option, // Changed from Option to Option + pub collection: Option, + pub uses: Option, + pub token_program_version: u8, // Assuming TokenProgramVersion is a simple u8 + pub creators: Vec, +} + #[derive(AnchorSerialize, AnchorDeserialize)] pub struct SolCnftFulfillBuyArgs { // === cNFT transfer args === // @@ -45,6 +82,8 @@ pub struct SolCnftFulfillBuyArgs { creator_verified: Vec, // Creator royalties. Validated against the metadata_hash by Bubblegum after hashing with metadata_hash. seller_fee_basis_points: u16, + + pub metadata_args: MetadataArgs, } #[derive(Accounts)] diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index f9d6b9b..61877b4 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2531,6 +2531,134 @@ export type Mmm = { ] } }, + { + "name": "CollectionArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "verified", + "type": "bool" + }, + { + "name": "key", + "type": "publicKey" + } + ] + } + }, + { + "name": "UsesArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "useMethod", + "type": "u8" + }, + { + "name": "remaining", + "type": "u64" + }, + { + "name": "total", + "type": "u64" + } + ] + } + }, + { + "name": "CreatorArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": "publicKey" + }, + { + "name": "verified", + "type": "bool" + }, + { + "name": "share", + "type": "u8" + } + ] + } + }, + { + "name": "MetadataArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "sellerFeeBasisPoints", + "type": "u16" + }, + { + "name": "primarySaleHappened", + "type": "bool" + }, + { + "name": "isMutable", + "type": "bool" + }, + { + "name": "editionNonce", + "type": { + "option": "u64" + } + }, + { + "name": "tokenStandard", + "type": { + "option": "u8" + } + }, + { + "name": "collection", + "type": { + "option": { + "defined": "CollectionArgs" + } + } + }, + { + "name": "uses", + "type": { + "option": { + "defined": "UsesArgs" + } + } + }, + { + "name": "tokenProgramVersion", + "type": "u8" + }, + { + "name": "creators", + "type": { + "vec": { + "defined": "CreatorArgs" + } + } + } + ] + } + }, { "name": "SolCnftFulfillBuyArgs", "type": { @@ -2616,6 +2744,12 @@ export type Mmm = { { "name": "sellerFeeBasisPoints", "type": "u16" + }, + { + "name": "metadataArgs", + "type": { + "defined": "MetadataArgs" + } } ] } @@ -5635,6 +5769,134 @@ export const IDL: Mmm = { ] } }, + { + "name": "CollectionArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "verified", + "type": "bool" + }, + { + "name": "key", + "type": "publicKey" + } + ] + } + }, + { + "name": "UsesArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "useMethod", + "type": "u8" + }, + { + "name": "remaining", + "type": "u64" + }, + { + "name": "total", + "type": "u64" + } + ] + } + }, + { + "name": "CreatorArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "address", + "type": "publicKey" + }, + { + "name": "verified", + "type": "bool" + }, + { + "name": "share", + "type": "u8" + } + ] + } + }, + { + "name": "MetadataArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + }, + { + "name": "sellerFeeBasisPoints", + "type": "u16" + }, + { + "name": "primarySaleHappened", + "type": "bool" + }, + { + "name": "isMutable", + "type": "bool" + }, + { + "name": "editionNonce", + "type": { + "option": "u64" + } + }, + { + "name": "tokenStandard", + "type": { + "option": "u8" + } + }, + { + "name": "collection", + "type": { + "option": { + "defined": "CollectionArgs" + } + } + }, + { + "name": "uses", + "type": { + "option": { + "defined": "UsesArgs" + } + } + }, + { + "name": "tokenProgramVersion", + "type": "u8" + }, + { + "name": "creators", + "type": { + "vec": { + "defined": "CreatorArgs" + } + } + } + ] + } + }, { "name": "SolCnftFulfillBuyArgs", "type": { @@ -5720,6 +5982,12 @@ export const IDL: Mmm = { { "name": "sellerFeeBasisPoints", "type": "u16" + }, + { + "name": "metadataArgs", + "type": { + "defined": "MetadataArgs" + } } ] } diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index db1e8fa..9bf66ec 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -1,5 +1,5 @@ import * as anchor from '@project-serum/anchor'; -import { publicKey, sol, Umi } from '@metaplex-foundation/umi'; +import { isSome, publicKey, sol, Umi } from '@metaplex-foundation/umi'; import { airdrop, createPool, @@ -33,6 +33,8 @@ import { import { findLeafAssetIdPda, getAssetWithProof, + getMetadataArgsSerializer, + MetadataArgs, MPL_BUBBLEGUM_PROGRAM_ID, SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, SPL_NOOP_PROGRAM_ID, @@ -204,6 +206,10 @@ describe('cnft tests', () => { console.log(`got creator royalties`); try { + const metadataSerializer = getMetadataArgsSerializer(); + const metadataArgs: MetadataArgs = metadataSerializer.deserialize( + metadataSerializer.serialize(metadata), + )[0]; const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), @@ -221,6 +227,33 @@ describe('cnft tests', () => { creatorShares, creatorVerified, sellerFeeBasisPoints, + metadataArgs: { + name: metadataArgs.name, + symbol: metadataArgs.symbol, + uri: metadataArgs.uri, + sellerFeeBasisPoints: metadataArgs.sellerFeeBasisPoints, + primarySaleHappened: metadataArgs.primarySaleHappened, + isMutable: metadataArgs.isMutable, + editionNonce: isSome(metadataArgs.editionNonce) + ? new BN(metadataArgs.editionNonce.value) + : null, + tokenStandard: isSome(metadataArgs.tokenStandard) + ? metadataArgs.tokenStandard.value + : null, + collection: isSome(metadataArgs.collection) + ? { + verified: metadataArgs.collection.value.verified, + key: new PublicKey(metadataArgs.collection.value.key), + } + : null, // Ensure it's a struct or null + uses: isSome(metadataArgs.uses) ? metadataArgs.uses.value : null, + tokenProgramVersion: metadataArgs.tokenProgramVersion ?? null, + creators: metadataArgs.creators.map((c) => ({ + address: new PublicKey(c.address), + verified: c.verified, + share: c.share, + })), + }, }) .accountsStrict({ payer: new PublicKey(seller.publicKey), From 6e069187548fbd2f4e08ec78f584c0b81152f5f7 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Thu, 7 Nov 2024 00:39:46 -0800 Subject: [PATCH 14/35] fix passing metadata args --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 59 +++++-- sdk/src/cnft.ts | 51 +++++- sdk/src/idl/mmm.ts | 164 +++++++++++++++--- tests/mmm-cnft.spec.ts | 32 +++- 4 files changed, 264 insertions(+), 42 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 2c09fc8..e1aa6f4 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -10,24 +10,49 @@ use crate::{ verify_referral::verify_referral, }; -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct CollectionArgs { +// Define the TokenStandard enum +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] +pub enum TokenStandard { + NonFungible, + FungibleAsset, + Fungible, + NonFungibleEdition, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq)] +pub struct Collection { pub verified: bool, - pub key: Pubkey, // Assuming PublicKey is equivalent to Pubkey in Rust + pub key: Pubkey, } -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct UsesArgs { - pub use_method: u8, - pub remaining: u64, // Use u64 for large numbers +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] +pub enum UseMethod { + Burn, + Multiple, + Single, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq)] +pub struct Uses { + pub use_method: UseMethod, + pub remaining: u64, pub total: u64, } -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct CreatorArgs { - pub address: Pubkey, // Assuming PublicKey is equivalent to Pubkey in Rust +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] +pub enum TokenProgramVersion { + Original, + Token2022, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq)] +pub struct Creator { + pub address: Pubkey, pub verified: bool, - pub share: u8, // Assuming share is a percentage, use u8 + /// The percentage share. + /// + /// The value is a percentage, not basis points. + pub share: u8, } // Define the MetadataArgs struct @@ -39,12 +64,12 @@ pub struct MetadataArgs { pub seller_fee_basis_points: u16, pub primary_sale_happened: bool, // Changed from Option to bool pub is_mutable: bool, // Changed from Option to bool - pub edition_nonce: Option, - pub token_standard: Option, // Changed from Option to Option - pub collection: Option, - pub uses: Option, - pub token_program_version: u8, // Assuming TokenProgramVersion is a simple u8 - pub creators: Vec, + pub edition_nonce: Option, + pub token_standard: Option, // Changed from Option to Option + pub collection: Option, + pub uses: Option, + pub token_program_version: TokenProgramVersion, // Assuming TokenProgramVersion is a simple u8 + pub creators: Vec, } #[derive(AnchorSerialize, AnchorDeserialize)] diff --git a/sdk/src/cnft.ts b/sdk/src/cnft.ts index 05b00d5..b93c64c 100644 --- a/sdk/src/cnft.ts +++ b/sdk/src/cnft.ts @@ -1,4 +1,9 @@ -import { MPL_BUBBLEGUM_PROGRAM_ID } from '@metaplex-foundation/mpl-bubblegum'; +import { + MPL_BUBBLEGUM_PROGRAM_ID, + TokenProgramVersion, + TokenStandard, + UseMethod, +} from '@metaplex-foundation/mpl-bubblegum'; import { AccountMeta, PublicKey } from '@solana/web3.js'; import { PREFIXES } from './constants'; import { BN } from '@project-serum/anchor'; @@ -84,3 +89,47 @@ export interface CreatorRoyaltyConfig { creators: Creator[]; sellerFeeBasisPoints: number; } + +// Function to convert a simple enum value to the IDL structure +export function convertToDecodeTokenStandardEnum(tokenStandard: TokenStandard) { + TokenStandard; + switch (tokenStandard) { + case TokenStandard.NonFungible: + return { nonFungible: {} }; + case TokenStandard.FungibleAsset: + return { fungibleAsset: {} }; + case TokenStandard.Fungible: + return { fungible: {} }; + case TokenStandard.NonFungibleEdition: + return { nonfungibleEdition: {} }; + default: + throw new Error('Unknown TokenStandard value'); + } +} + +// Function to convert UseMethod to the DecodeEnum format +export function convertToDecodeUseMethodEnum(useMethod: UseMethod) { + switch (useMethod) { + case UseMethod.Burn: + return { burn: {} }; + case UseMethod.Multiple: + return { multiple: {} }; + case UseMethod.Single: + return { single: {} }; + default: + throw new Error('Unknown UseMethod value'); + } +} + +export function convertToDecodeTokenProgramVersion( + tokenProgramVersion: TokenProgramVersion, +) { + switch (tokenProgramVersion) { + case TokenProgramVersion.Original: + return { original: {} }; + case TokenProgramVersion.Token2022: + return { token2022: {} }; + default: + throw new Error('Unknown TokenProgramVersion value'); + } +} diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 61877b4..f8b6dde 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2532,7 +2532,7 @@ export type Mmm = { } }, { - "name": "CollectionArgs", + "name": "Collection", "type": { "kind": "struct", "fields": [ @@ -2548,13 +2548,15 @@ export type Mmm = { } }, { - "name": "UsesArgs", + "name": "Uses", "type": { "kind": "struct", "fields": [ { "name": "useMethod", - "type": "u8" + "type": { + "defined": "UseMethod" + } }, { "name": "remaining", @@ -2568,7 +2570,7 @@ export type Mmm = { } }, { - "name": "CreatorArgs", + "name": "Creator", "type": { "kind": "struct", "fields": [ @@ -2582,6 +2584,11 @@ export type Mmm = { }, { "name": "share", + "docs": [ + "The percentage share.", + "", + "The value is a percentage, not basis points." + ], "type": "u8" } ] @@ -2619,20 +2626,22 @@ export type Mmm = { { "name": "editionNonce", "type": { - "option": "u64" + "option": "u8" } }, { "name": "tokenStandard", "type": { - "option": "u8" + "option": { + "defined": "TokenStandard" + } } }, { "name": "collection", "type": { "option": { - "defined": "CollectionArgs" + "defined": "Collection" } } }, @@ -2640,19 +2649,21 @@ export type Mmm = { "name": "uses", "type": { "option": { - "defined": "UsesArgs" + "defined": "Uses" } } }, { "name": "tokenProgramVersion", - "type": "u8" + "type": { + "defined": "TokenProgramVersion" + } }, { "name": "creators", "type": { "vec": { - "defined": "CreatorArgs" + "defined": "Creator" } } } @@ -3055,6 +3066,57 @@ export type Mmm = { } ] } + }, + { + "name": "TokenStandard", + "type": { + "kind": "enum", + "variants": [ + { + "name": "NonFungible" + }, + { + "name": "FungibleAsset" + }, + { + "name": "Fungible" + }, + { + "name": "NonFungibleEdition" + } + ] + } + }, + { + "name": "UseMethod", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Burn" + }, + { + "name": "Multiple" + }, + { + "name": "Single" + } + ] + } + }, + { + "name": "TokenProgramVersion", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Original" + }, + { + "name": "Token2022" + } + ] + } } ], "errors": [ @@ -5770,7 +5832,7 @@ export const IDL: Mmm = { } }, { - "name": "CollectionArgs", + "name": "Collection", "type": { "kind": "struct", "fields": [ @@ -5786,13 +5848,15 @@ export const IDL: Mmm = { } }, { - "name": "UsesArgs", + "name": "Uses", "type": { "kind": "struct", "fields": [ { "name": "useMethod", - "type": "u8" + "type": { + "defined": "UseMethod" + } }, { "name": "remaining", @@ -5806,7 +5870,7 @@ export const IDL: Mmm = { } }, { - "name": "CreatorArgs", + "name": "Creator", "type": { "kind": "struct", "fields": [ @@ -5820,6 +5884,11 @@ export const IDL: Mmm = { }, { "name": "share", + "docs": [ + "The percentage share.", + "", + "The value is a percentage, not basis points." + ], "type": "u8" } ] @@ -5857,20 +5926,22 @@ export const IDL: Mmm = { { "name": "editionNonce", "type": { - "option": "u64" + "option": "u8" } }, { "name": "tokenStandard", "type": { - "option": "u8" + "option": { + "defined": "TokenStandard" + } } }, { "name": "collection", "type": { "option": { - "defined": "CollectionArgs" + "defined": "Collection" } } }, @@ -5878,19 +5949,21 @@ export const IDL: Mmm = { "name": "uses", "type": { "option": { - "defined": "UsesArgs" + "defined": "Uses" } } }, { "name": "tokenProgramVersion", - "type": "u8" + "type": { + "defined": "TokenProgramVersion" + } }, { "name": "creators", "type": { "vec": { - "defined": "CreatorArgs" + "defined": "Creator" } } } @@ -6293,6 +6366,57 @@ export const IDL: Mmm = { } ] } + }, + { + "name": "TokenStandard", + "type": { + "kind": "enum", + "variants": [ + { + "name": "NonFungible" + }, + { + "name": "FungibleAsset" + }, + { + "name": "Fungible" + }, + { + "name": "NonFungibleEdition" + } + ] + } + }, + { + "name": "UseMethod", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Burn" + }, + { + "name": "Multiple" + }, + { + "name": "Single" + } + ] + } + }, + { + "name": "TokenProgramVersion", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Original" + }, + { + "name": "Token2022" + } + ] + } } ], "errors": [ diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 9bf66ec..913a7cc 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -11,6 +11,9 @@ import { verifyOwnership, } from './utils'; import { + convertToDecodeTokenProgramVersion, + convertToDecodeTokenStandardEnum, + convertToDecodeUseMethodEnum, getBubblegumAuthorityPDA, getByteArray, getM2BuyerSharedEscrow, @@ -210,6 +213,15 @@ describe('cnft tests', () => { const metadataArgs: MetadataArgs = metadataSerializer.deserialize( metadataSerializer.serialize(metadata), )[0]; + + console.log(`metadataArgs: ${JSON.stringify(metadataArgs)}`); + console.log( + `${JSON.stringify( + convertToDecodeTokenProgramVersion( + metadataArgs.tokenProgramVersion, + ) )}`, + ); + const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), @@ -235,10 +247,12 @@ describe('cnft tests', () => { primarySaleHappened: metadataArgs.primarySaleHappened, isMutable: metadataArgs.isMutable, editionNonce: isSome(metadataArgs.editionNonce) - ? new BN(metadataArgs.editionNonce.value) + ? metadataArgs.editionNonce.value : null, tokenStandard: isSome(metadataArgs.tokenStandard) - ? metadataArgs.tokenStandard.value + ? convertToDecodeTokenStandardEnum( + metadataArgs.tokenStandard.value, + ) : null, collection: isSome(metadataArgs.collection) ? { @@ -246,8 +260,18 @@ describe('cnft tests', () => { key: new PublicKey(metadataArgs.collection.value.key), } : null, // Ensure it's a struct or null - uses: isSome(metadataArgs.uses) ? metadataArgs.uses.value : null, - tokenProgramVersion: metadataArgs.tokenProgramVersion ?? null, + uses: isSome(metadataArgs.uses) + ? { + useMethod: convertToDecodeUseMethodEnum( + metadataArgs.uses.value.useMethod, + ), + remaining: metadataArgs.uses.value.remaining, + total: metadataArgs.uses.value.total, + } + : null, + tokenProgramVersion: convertToDecodeTokenProgramVersion( + metadataArgs.tokenProgramVersion, + ), creators: metadataArgs.creators.map((c) => ({ address: new PublicKey(c.address), verified: c.verified, From c58fdc8cc376d7e6f2a53273684d8fbdab0ac97a Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Thu, 7 Nov 2024 13:47:27 -0800 Subject: [PATCH 15/35] add collection metadata verification --- .../src/instructions/cnft/metadata_args.rs | 63 +++++++++++++++++ programs/mmm/src/instructions/cnft/mod.rs | 4 +- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 68 ++----------------- programs/mmm/src/util.rs | 21 +++--- 4 files changed, 84 insertions(+), 72 deletions(-) create mode 100644 programs/mmm/src/instructions/cnft/metadata_args.rs diff --git a/programs/mmm/src/instructions/cnft/metadata_args.rs b/programs/mmm/src/instructions/cnft/metadata_args.rs new file mode 100644 index 0000000..877c376 --- /dev/null +++ b/programs/mmm/src/instructions/cnft/metadata_args.rs @@ -0,0 +1,63 @@ +use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; + +// Define the TokenStandard enum +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] +pub enum TokenStandard { + NonFungible, + FungibleAsset, + Fungible, + NonFungibleEdition, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq)] +pub struct Collection { + pub verified: bool, + pub key: Pubkey, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] +pub enum UseMethod { + Burn, + Multiple, + Single, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq)] +pub struct Uses { + pub use_method: UseMethod, + pub remaining: u64, + pub total: u64, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] +pub enum TokenProgramVersion { + Original, + Token2022, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq)] +pub struct Creator { + pub address: Pubkey, + pub verified: bool, + /// The percentage share. + /// + /// The value is a percentage, not basis points. + pub share: u8, +} + +// Define the MetadataArgs struct +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct MetadataArgs { + pub name: String, + pub symbol: String, // Changed from Option to String + pub uri: String, + pub seller_fee_basis_points: u16, + pub primary_sale_happened: bool, // Changed from Option to bool + pub is_mutable: bool, // Changed from Option to bool + pub edition_nonce: Option, + pub token_standard: Option, // Changed from Option to Option + pub collection: Option, + pub uses: Option, + pub token_program_version: TokenProgramVersion, // Assuming TokenProgramVersion is a simple u8 + pub creators: Vec, +} diff --git a/programs/mmm/src/instructions/cnft/mod.rs b/programs/mmm/src/instructions/cnft/mod.rs index 51651c0..3dd87ea 100644 --- a/programs/mmm/src/instructions/cnft/mod.rs +++ b/programs/mmm/src/instructions/cnft/mod.rs @@ -1,3 +1,5 @@ pub mod sol_cnft_fulfill_buy; +pub mod metadata_args; -pub use sol_cnft_fulfill_buy::*; \ No newline at end of file +pub use sol_cnft_fulfill_buy::*; +pub use metadata_args::*; \ No newline at end of file diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index e1aa6f4..6c0dfba 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -6,71 +6,11 @@ use crate::{ constants::*, errors::MMMErrorCode, state::{BubblegumProgram, Pool, SellState, TreeConfigAnchor}, - util::{log_pool, transfer_compressed_nft, try_close_pool}, + util::{hash_metadata, log_pool, transfer_compressed_nft, try_close_pool}, verify_referral::verify_referral, }; -// Define the TokenStandard enum -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] -pub enum TokenStandard { - NonFungible, - FungibleAsset, - Fungible, - NonFungibleEdition, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq)] -pub struct Collection { - pub verified: bool, - pub key: Pubkey, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] -pub enum UseMethod { - Burn, - Multiple, - Single, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq)] -pub struct Uses { - pub use_method: UseMethod, - pub remaining: u64, - pub total: u64, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] -pub enum TokenProgramVersion { - Original, - Token2022, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq)] -pub struct Creator { - pub address: Pubkey, - pub verified: bool, - /// The percentage share. - /// - /// The value is a percentage, not basis points. - pub share: u8, -} - -// Define the MetadataArgs struct -#[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct MetadataArgs { - pub name: String, - pub symbol: String, // Changed from Option to String - pub uri: String, - pub seller_fee_basis_points: u16, - pub primary_sale_happened: bool, // Changed from Option to bool - pub is_mutable: bool, // Changed from Option to bool - pub edition_nonce: Option, - pub token_standard: Option, // Changed from Option to Option - pub collection: Option, - pub uses: Option, - pub token_program_version: TokenProgramVersion, // Assuming TokenProgramVersion is a simple u8 - pub creators: Vec, -} +use super::MetadataArgs; #[derive(AnchorSerialize, AnchorDeserialize)] pub struct SolCnftFulfillBuyArgs { @@ -210,6 +150,8 @@ pub fn handler<'info>( return Err(MMMErrorCode::InvalidAccountState.into()); } + let data_hash = hash_metadata(&args.metadata_args)?; + msg!("seller fee basis points: {}", args.seller_fee_basis_points); // Create data_hash from metadata_hash + seller_fee_basis_points (secures creator royalties) // let data_hash = hash_metadata_data(args.metadata_hash, args.seller_fee_basis_points)?; @@ -228,7 +170,7 @@ pub fn handler<'info>( proof_path, ctx.accounts.bubblegum_program.key(), args.root, - args.metadata_hash, + data_hash, args.creator_hash, args.nonce, args.index, diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 71d10a3..88b8971 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -7,7 +7,7 @@ use crate::{ errors::MMMErrorCode, get_creators_from_royalties, state::*, - IndexableAsset, + IndexableAsset, MetadataArgs, }; use anchor_lang::{prelude::*, solana_program::log::sol_log_data}; use anchor_spl::token_interface::Mint; @@ -1214,13 +1214,18 @@ pub fn transfer_compressed_nft<'info>( Ok(()) } -// Taken from Bubblegum's hash_metadata: hashes seller_fee_basis_points to the final data_hash that Bubblegum expects. -// This way we can use the seller_fee_basis_points while still guaranteeing validity. -pub fn hash_metadata_data( - metadata_args_hash: [u8; 32], - seller_fee_basis_points: u16, -) -> Result<[u8; 32]> { - Ok(keccak::hashv(&[&metadata_args_hash, &seller_fee_basis_points.to_le_bytes()]).to_bytes()) +/// Computes the hash of the metadata. +/// +/// The hash is computed as the keccak256 hash of the metadata bytes, which is +/// then hashed with the `seller_fee_basis_points`. +pub fn hash_metadata(metadata: &MetadataArgs) -> Result<[u8; 32]> { + let hash = keccak::hashv(&[metadata.try_to_vec()?.as_slice()]); + // Calculate new data hash. + Ok(keccak::hashv(&[ + &hash.to_bytes(), + &metadata.seller_fee_basis_points.to_le_bytes(), + ]) + .to_bytes()) } #[cfg(test)] From c9dc16ca5d3069e3f0966a46190c9aaa4e79a6ff Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Thu, 7 Nov 2024 14:51:59 -0800 Subject: [PATCH 16/35] add fee cacluation, shared escrow support, reinvest buy support --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 167 +++++++++++++++--- tests/mmm-cnft.spec.ts | 14 +- 2 files changed, 146 insertions(+), 35 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 6c0dfba..89d1c8c 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -1,12 +1,18 @@ use std::str::FromStr; use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; +use mpl_bubblegum::utils::get_asset_id; use crate::{ constants::*, errors::MMMErrorCode, + index_ra, state::{BubblegumProgram, Pool, SellState, TreeConfigAnchor}, - util::{hash_metadata, log_pool, transfer_compressed_nft, try_close_pool}, + util::{ + assert_valid_fees_bp, check_remaining_accounts_for_m2, get_buyside_seller_receives, + get_lp_fee_bp, get_sol_fee, get_sol_lp_fee, get_sol_total_price_and_next_price, + hash_metadata, log_pool, transfer_compressed_nft, try_close_pool, withdraw_m2, + }, verify_referral::verify_referral, }; @@ -127,9 +133,11 @@ pub struct SolCnftFulfillBuy<'info> { // Branch: using shared escrow accounts // 0: m2_program // 1: shared_escrow_account - // 2+: creator accounts + // 2-N: creator accounts + //. N+: proof accounts // Branch: not using shared escrow accounts - // 0+: creator accounts + // 0-N: creator accounts + //. N+: proof accounts } pub fn handler<'info>( @@ -138,19 +146,140 @@ pub fn handler<'info>( ) -> Result<()> { // let payer = &ctx.accounts.payer; let owner = &ctx.accounts.owner; - let pool = &ctx.accounts.pool; - // let sell_state = &mut ctx.accounts.sell_state; - // let merkle_tree = &ctx.accounts.merkle_tree; + let pool = &mut ctx.accounts.pool; + let buyside_sol_escrow_account = &ctx.accounts.buyside_sol_escrow_account; + let sell_state = &mut ctx.accounts.sell_state; + let merkle_tree = &ctx.accounts.merkle_tree; // Remaining accounts are 1. (Optional) creator addresses and 2. Merkle proof path. let creator_shares_length = args.creator_shares.len(); let creator_shares_clone = args.creator_shares.clone(); - let (creator_accounts, proof_path) = ctx.remaining_accounts.split_at(creator_shares_length); + let remaining_accounts = ctx.remaining_accounts; + let system_program = &ctx.accounts.system_program; - if pool.using_shared_escrow() { - return Err(MMMErrorCode::InvalidAccountState.into()); + let data_hash = hash_metadata(&args.metadata_args)?; + let asset_mint = get_asset_id(&merkle_tree.key(), args.nonce); + + // 1. Cacluate seller receives + let (total_price, next_price) = + get_sol_total_price_and_next_price(pool, args.asset_amount, true)?; + let metadata_royalty_bp = args.metadata_args.seller_fee_basis_points; + // TODO: update lp_fee_bp when shared escrow for both side is enabled + let seller_receives = { + let lp_fee_bp = get_lp_fee_bp(pool, buyside_sol_escrow_account.lamports()); + get_buyside_seller_receives( + total_price, + lp_fee_bp, + metadata_royalty_bp, + pool.buyside_creator_royalty_bp, + ) + }?; + + // 2. Calculate fees + let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; + assert_valid_fees_bp(args.maker_fee_bp, args.taker_fee_bp)?; + let maker_fee = get_sol_fee(seller_receives, args.maker_fee_bp)?; + let taker_fee = get_sol_fee(seller_receives, args.taker_fee_bp)?; + let referral_fee = u64::try_from( + maker_fee + .checked_add(taker_fee) + .ok_or(MMMErrorCode::NumericOverflow)?, + ) + .map_err(|_| MMMErrorCode::NumericOverflow)?; + + // 3. Get creator accounts, verify creators + // check creator_accounts and verify the remaining accounts + // ... existing code ... + let (creator_accounts, proof_path) = if pool.using_shared_escrow() { + check_remaining_accounts_for_m2(remaining_accounts, &pool.owner.key())?; + + let amount: u64 = (total_price as i64 + maker_fee) as u64; + withdraw_m2( + pool, + ctx.bumps.pool, + buyside_sol_escrow_account, + index_ra!(remaining_accounts, 1), + system_program, + index_ra!(remaining_accounts, 0), + pool.owner, + amount, + )?; + pool.shared_escrow_count = pool + .shared_escrow_count + .checked_sub(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + + remaining_accounts[2..].split_at(creator_shares_length + 2) + } else { + remaining_accounts.split_at(creator_shares_length) + }; + + // 4. Transfer CNFT to buyer (pool or owner) + if pool.reinvest_fulfill_buy { + if pool.using_shared_escrow() { + return Err(MMMErrorCode::InvalidAccountState.into()); + } + transfer_compressed_nft( + &ctx.accounts.tree_authority.to_account_info(), + &pool.to_account_info(), + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.owner.to_account_info(), + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + &ctx.accounts.system_program, // Pass as Program without calling to_account_info() + proof_path, + ctx.accounts.bubblegum_program.key(), + args.root, + data_hash, + args.creator_hash, + args.nonce, + args.index, + None, // signer passed through from ctx + )?; + pool.sellside_asset_amount = pool + .sellside_asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + sell_state.pool = pool.key(); + sell_state.pool_owner = owner.key(); + sell_state.asset_mint = asset_mint.key(); + sell_state.cosigner_annotation = pool.cosigner_annotation; + sell_state.asset_amount = sell_state + .asset_amount + .checked_add(args.asset_amount) + .ok_or(MMMErrorCode::NumericOverflow)?; + } else { + transfer_compressed_nft( + &ctx.accounts.tree_authority.to_account_info(), + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.owner.to_account_info(), + &ctx.accounts.merkle_tree, + &ctx.accounts.log_wrapper, + &ctx.accounts.compression_program, + &ctx.accounts.system_program, // Pass as Program without calling to_account_info() + proof_path, + ctx.accounts.bubblegum_program.key(), + args.root, + data_hash, + args.creator_hash, + args.nonce, + args.index, + None, // signer passed through from ctx + )?; } - let data_hash = hash_metadata(&args.metadata_args)?; + // 5. Pool owner as buyer pay royalties to creators + // 6. Prevent frontrun by pool config changes + // 7. Close pool if all NFTs are sold + // 8. Pool pay the sol to the seller + // 9. pay lp fee + // 10. pay referral fee + // 11. update pool state + // 12 try close buy side escrow account + // 13. try close sell state account + // 14. return the remaining per pool escrow balance to the shared escrow account + // 15. update pool and log pool and try close pool msg!("seller fee basis points: {}", args.seller_fee_basis_points); // Create data_hash from metadata_hash + seller_fee_basis_points (secures creator royalties) @@ -158,24 +287,6 @@ pub fn handler<'info>( // Transfer CNFT from seller(payer) to buyer (pool owner) // TODO: do I need to send to pool instead? - transfer_compressed_nft( - &ctx.accounts.tree_authority.to_account_info(), - &ctx.accounts.payer.to_account_info(), - &ctx.accounts.payer.to_account_info(), - &ctx.accounts.owner.to_account_info(), - &ctx.accounts.merkle_tree, - &ctx.accounts.log_wrapper, - &ctx.accounts.compression_program, - &ctx.accounts.system_program, // Pass as Program without calling to_account_info() - proof_path, - ctx.accounts.bubblegum_program.key(), - args.root, - data_hash, - args.creator_hash, - args.nonce, - args.index, - None, // signer passed through from ctx - )?; log_pool("post_sol_cnft_fulfill_buy", pool)?; try_close_pool(pool, owner.to_account_info())?; diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 913a7cc..64207cd 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -53,6 +53,7 @@ async function createCNftCollectionOffer( ) { const poolData = await createPool(program, { ...poolArgs, + reinvestFulfillBuy: false, }); const poolKey = poolData.poolKey; @@ -215,13 +216,12 @@ describe('cnft tests', () => { )[0]; console.log(`metadataArgs: ${JSON.stringify(metadataArgs)}`); - console.log( - `${JSON.stringify( - convertToDecodeTokenProgramVersion( - metadataArgs.tokenProgramVersion, - ) )}`, - ); - + console.log( + `${JSON.stringify( + convertToDecodeTokenProgramVersion(metadataArgs.tokenProgramVersion), + )}`, + ); + const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), From 558bb62a0b658e3692d4ef6f0f0bae45a9c7c2fa Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Fri, 8 Nov 2024 14:34:31 -0800 Subject: [PATCH 17/35] add creator verification and pay --- programs/mmm/src/errors.rs | 4 + .../instructions/cnft/sol_cnft_fulfill_buy.rs | 24 +++- programs/mmm/src/util.rs | 122 +++++++++++++++++- sdk/src/idl/mmm.ts | 20 +++ 4 files changed, 168 insertions(+), 2 deletions(-) diff --git a/programs/mmm/src/errors.rs b/programs/mmm/src/errors.rs index 40d4031..829e634 100644 --- a/programs/mmm/src/errors.rs +++ b/programs/mmm/src/errors.rs @@ -72,4 +72,8 @@ pub enum MMMErrorCode { InvalidTokenExtension, // 0x1791 #[msg("Unsupported asset plugin")] UnsupportedAssetPlugin, // 0x1792 + #[msg("Mismatched ceator data lengths")] + MismatchedCreatorDataLengths, // 0x1793 + #[msg("Invalid creators")] + InvalidCreators, // 0x1794 } diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 89d1c8c..75a17bb 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -11,7 +11,8 @@ use crate::{ util::{ assert_valid_fees_bp, check_remaining_accounts_for_m2, get_buyside_seller_receives, get_lp_fee_bp, get_sol_fee, get_sol_lp_fee, get_sol_total_price_and_next_price, - hash_metadata, log_pool, transfer_compressed_nft, try_close_pool, withdraw_m2, + hash_metadata, log_pool, pay_creator_fees_in_sol_cnft, transfer_compressed_nft, + try_close_pool, verify_creators, withdraw_m2, }, verify_referral::verify_referral, }; @@ -158,6 +159,12 @@ pub fn handler<'info>( let data_hash = hash_metadata(&args.metadata_args)?; let asset_mint = get_asset_id(&merkle_tree.key(), args.nonce); + let pool_key = pool.key(); + let buyside_sol_escrow_account_seeds: &[&[&[u8]]] = &[&[ + BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), + pool_key.as_ref(), + &[ctx.bumps.buyside_sol_escrow_account], + ]]; // 1. Cacluate seller receives let (total_price, next_price) = @@ -212,6 +219,12 @@ pub fn handler<'info>( } else { remaining_accounts.split_at(creator_shares_length) }; + verify_creators( + creator_accounts.iter(), + args.creator_shares, + args.creator_verified, + args.creator_hash, + )?; // 4. Transfer CNFT to buyer (pool or owner) if pool.reinvest_fulfill_buy { @@ -270,6 +283,15 @@ pub fn handler<'info>( } // 5. Pool owner as buyer pay royalties to creators + let royalty_paid = pay_creator_fees_in_sol_cnft( + pool.buyside_creator_royalty_bp, + seller_receives, + &args.metadata_args, + creator_accounts, + buyside_sol_escrow_account.to_account_info(), + buyside_sol_escrow_account_seeds, + system_program.to_account_info(), + )?; // 6. Prevent frontrun by pool config changes // 7. Close pool if all NFTs are sold // 8. Pool pay the sol to the seller diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 88b8971..a577903 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -14,6 +14,7 @@ use anchor_spl::token_interface::Mint; use m2_interface::{ withdraw_by_mmm_ix_with_program_id, WithdrawByMMMArgs, WithdrawByMmmIxArgs, WithdrawByMmmKeys, }; +use mpl_bubblegum::hash::hash_creators; use mpl_core::types::{Royalties, UpdateAuthority}; use mpl_token_metadata::{ accounts::{MasterEdition, Metadata}, @@ -30,7 +31,7 @@ use spl_token_2022::{ }; use spl_token_group_interface::state::TokenGroupMember; use spl_token_metadata_interface::state::TokenMetadata; -use std::{convert::TryFrom, str::FromStr}; +use std::{convert::TryFrom, slice::Iter, str::FromStr}; #[macro_export] macro_rules! index_ra { @@ -597,6 +598,83 @@ pub fn pay_creator_fees_in_sol<'info>( Ok(total_royalty) } +#[allow(clippy::too_many_arguments)] +pub fn pay_creator_fees_in_sol_cnft<'info>( + buyside_creator_royalty_bp: u16, + total_price: u64, + metadata_args: &MetadataArgs, + creator_accounts: &[AccountInfo<'info>], + payer: AccountInfo<'info>, + payer_seeds: &[&[&[u8]]], + system_program: AccountInfo<'info>, +) -> Result { + // Calculate the total royalty to be paid + let royalty = ((total_price as u128) + .checked_mul(metadata_args.seller_fee_basis_points as u128) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_div(10000) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_mul(buyside_creator_royalty_bp as u128) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_div(10000) + .ok_or(MMMErrorCode::NumericOverflow)?) as u64; + + if royalty == 0 { + return Ok(0); + } + + if payer.lamports() < royalty { + return Err(MMMErrorCode::NotEnoughBalance.into()); + } + + let min_rent = Rent::get()?.minimum_balance(0); + let mut total_royalty: u64 = 0; + + let creator_accounts_iter = &mut creator_accounts.iter(); + for (index, creator) in metadata_args.creators.iter().enumerate() { + let creator_fee = if index == metadata_args.creators.len() - 1 { + royalty + .checked_sub(total_royalty) + .ok_or(MMMErrorCode::NumericOverflow)? + } else { + (royalty as u128) + .checked_mul(creator.share as u128) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_div(100) + .ok_or(MMMErrorCode::NumericOverflow)? as u64 + }; + let current_creator_info = next_account_info(creator_accounts_iter)?; + if creator.address.ne(current_creator_info.key) { + return Err(MMMErrorCode::InvalidCreatorAddress.into()); + } + let current_creator_lamports = current_creator_info.lamports(); + if creator_fee > 0 + && current_creator_lamports + .checked_add(creator_fee) + .ok_or(MMMErrorCode::NumericOverflow)? + > min_rent + { + anchor_lang::solana_program::program::invoke_signed( + &anchor_lang::solana_program::system_instruction::transfer( + payer.key, + current_creator_info.key, + creator_fee, + ), + &[ + payer.to_account_info(), + current_creator_info.to_account_info(), + system_program.to_account_info(), + ], + payer_seeds, + )?; + total_royalty = total_royalty + .checked_add(creator_fee) + .ok_or(MMMErrorCode::NumericOverflow)?; + } + } + Ok(total_royalty) +} + pub fn log_pool(prefix: &str, pool: &Pool) -> Result<()> { msg!(prefix); sol_log_data(&[&pool.try_to_vec()?]); @@ -1228,6 +1306,48 @@ pub fn hash_metadata(metadata: &MetadataArgs) -> Result<[u8; 32]> { .to_bytes()) } +pub fn verify_creators( + creator_accounts: Iter, + creator_shares: Vec, + creator_verified: Vec, + creator_hash: [u8; 32], +) -> Result<()> { + // Check that all input arrays/vectors are of the same length + if creator_accounts.len() != creator_shares.len() + || creator_accounts.len() != creator_verified.len() + { + return Err(MMMErrorCode::MismatchedCreatorDataLengths.into()); + } + + // Convert input data to a vector of Creator structs + let creators: Vec = creator_accounts + .zip(creator_shares.iter()) + .zip(creator_verified.iter()) + .map( + |((account, &share), &verified)| mpl_bubblegum::types::Creator { + address: *account.key, + verified, + share: share as u8, // Assuming the share is never more than 255. If it can be, this needs additional checks. + }, + ) + .collect(); + + // Compute the hash from the Creator vector + let computed_hash = hash_creators(&creators); + + // Compare the computed hash with the provided hash + if computed_hash != creator_hash { + msg!( + "Computed hash does not match provided hash: {{\"computed\":{:?},\"provided\":{:?}}}", + computed_hash, + creator_hash + ); + return Err(MMMErrorCode::InvalidCreators.into()); + } + + Ok(()) +} + #[cfg(test)] mod tests { use anchor_spl::token_2022; diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index f8b6dde..86bddf4 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -3294,6 +3294,16 @@ export type Mmm = { "code": 6034, "name": "UnsupportedAssetPlugin", "msg": "Unsupported asset plugin" + }, + { + "code": 6035, + "name": "MismatchedCreatorDataLengths", + "msg": "Mismatched ceator data lengths" + }, + { + "code": 6036, + "name": "InvalidCreators", + "msg": "Invalid creators" } ] }; @@ -6594,6 +6604,16 @@ export const IDL: Mmm = { "code": 6034, "name": "UnsupportedAssetPlugin", "msg": "Unsupported asset plugin" + }, + { + "code": 6035, + "name": "MismatchedCreatorDataLengths", + "msg": "Mismatched ceator data lengths" + }, + { + "code": 6036, + "name": "InvalidCreators", + "msg": "Invalid creators" } ] }; From da09c32be0961be490a18362609cc9a0e6f48493 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Fri, 8 Nov 2024 15:31:43 -0800 Subject: [PATCH 18/35] fix full logic --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 132 +++++++++++++++--- tests/mmm-cnft.spec.ts | 16 ++- 2 files changed, 128 insertions(+), 20 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 75a17bb..622298e 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -12,7 +12,7 @@ use crate::{ assert_valid_fees_bp, check_remaining_accounts_for_m2, get_buyside_seller_receives, get_lp_fee_bp, get_sol_fee, get_sol_lp_fee, get_sol_total_price_and_next_price, hash_metadata, log_pool, pay_creator_fees_in_sol_cnft, transfer_compressed_nft, - try_close_pool, verify_creators, withdraw_m2, + try_close_escrow, try_close_pool, try_close_sell_state, verify_creators, withdraw_m2, }, verify_referral::verify_referral, }; @@ -150,6 +150,8 @@ pub fn handler<'info>( let pool = &mut ctx.accounts.pool; let buyside_sol_escrow_account = &ctx.accounts.buyside_sol_escrow_account; let sell_state = &mut ctx.accounts.sell_state; + let payer = &ctx.accounts.payer; + let referral: &UncheckedAccount<'info> = &ctx.accounts.referral; let merkle_tree = &ctx.accounts.merkle_tree; // Remaining accounts are 1. (Optional) creator addresses and 2. Merkle proof path. let creator_shares_length = args.creator_shares.len(); @@ -292,23 +294,121 @@ pub fn handler<'info>( buyside_sol_escrow_account_seeds, system_program.to_account_info(), )?; - // 6. Prevent frontrun by pool config changes - // 7. Close pool if all NFTs are sold - // 8. Pool pay the sol to the seller - // 9. pay lp fee - // 10. pay referral fee - // 11. update pool state - // 12 try close buy side escrow account - // 13. try close sell state account - // 14. return the remaining per pool escrow balance to the shared escrow account - // 15. update pool and log pool and try close pool - msg!("seller fee basis points: {}", args.seller_fee_basis_points); - // Create data_hash from metadata_hash + seller_fee_basis_points (secures creator royalties) - // let data_hash = hash_metadata_data(args.metadata_hash, args.seller_fee_basis_points)?; + // 6. Pay buyer, prevent frontrun by pool config changes + // the royalties are paid by the buyer, but the seller will see the price + // after adjusting the royalties. + let payment_amount = total_price + .checked_sub(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_sub(taker_fee as u64) + .ok_or(MMMErrorCode::NumericOverflow)? + .checked_sub(royalty_paid) + .ok_or(MMMErrorCode::NumericOverflow)?; + msg!("payment_amount: {}", payment_amount); + msg!("args.min_payment_amount: {}", args.min_payment_amount); + msg!("total_price: {}", total_price); + msg!("lp_fee: {}", lp_fee); + msg!("taker_fee: {}", taker_fee); + msg!("royalty_paid: {}", royalty_paid); + msg!("seller_receives {}", seller_receives); + if payment_amount < args.min_payment_amount { + return Err(MMMErrorCode::InvalidRequestedPrice.into()); + } + + anchor_lang::solana_program::program::invoke_signed( + &anchor_lang::solana_program::system_instruction::transfer( + buyside_sol_escrow_account.key, + &payer.key, + payment_amount, + ), + &[ + buyside_sol_escrow_account.to_account_info(), + payer.to_account_info(), + system_program.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + + // 7. pay lp fee + if lp_fee > 0 { + anchor_lang::solana_program::program::invoke_signed( + &anchor_lang::solana_program::system_instruction::transfer( + buyside_sol_escrow_account.key, + owner.key, + lp_fee, + ), + &[ + buyside_sol_escrow_account.to_account_info(), + owner.to_account_info(), + system_program.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + } - // Transfer CNFT from seller(payer) to buyer (pool owner) - // TODO: do I need to send to pool instead? + // 8. pay referral fee + if referral_fee > 0 { + anchor_lang::solana_program::program::invoke_signed( + &anchor_lang::solana_program::system_instruction::transfer( + buyside_sol_escrow_account.key, + referral.key, + referral_fee, + ), + &[ + buyside_sol_escrow_account.to_account_info(), + referral.to_account_info(), + system_program.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + } + + // 9. try close buy side escrow account + try_close_escrow( + &buyside_sol_escrow_account.to_account_info(), + pool, + system_program, + buyside_sol_escrow_account_seeds, + )?; + try_close_sell_state(sell_state, payer.to_account_info())?; + + // 10. return the remaining per pool escrow balance to the shared escrow account + if pool.using_shared_escrow() { + let min_rent = Rent::get()?.minimum_balance(0); + let shared_escrow_account = index_ra!(remaining_accounts, 1).to_account_info(); + if shared_escrow_account.lamports() + buyside_sol_escrow_account.lamports() > min_rent + && buyside_sol_escrow_account.lamports() > 0 + { + anchor_lang::solana_program::program::invoke_signed( + &anchor_lang::solana_program::system_instruction::transfer( + buyside_sol_escrow_account.key, + shared_escrow_account.key, + buyside_sol_escrow_account.lamports(), + ), + &[ + buyside_sol_escrow_account.to_account_info(), + shared_escrow_account, + system_program.to_account_info(), + ], + buyside_sol_escrow_account_seeds, + )?; + } else { + try_close_escrow( + buyside_sol_escrow_account, + pool, + system_program, + buyside_sol_escrow_account_seeds, + )?; + } + } + // 11. update pool state and log + pool.lp_fee_earned = pool + .lp_fee_earned + .checked_add(lp_fee) + .ok_or(MMMErrorCode::NumericOverflow)?; + pool.spot_price = next_price; + pool.buyside_payment_amount = buyside_sol_escrow_account.lamports(); log_pool("post_sol_cnft_fulfill_buy", pool)?; try_close_pool(pool, owner.to_account_info())?; diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 64207cd..fcb093f 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -173,14 +173,14 @@ describe('cnft tests', () => { nft.nft.nftIndex, ); - const spotPrice = 10; + const spotPrice = 1; const expectedBuyPrices = getSolFulfillBuyPrices({ totalPriceLamports: spotPrice * LAMPORTS_PER_SOL, - lpFeeBp: 0, + lpFeeBp: 200, takerFeeBp: 100, metadataRoyaltyBp: 500, buysideCreatorRoyaltyBp: 10_000, - makerFeeBp: 100, + makerFeeBp: 0, }); const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress( @@ -222,6 +222,14 @@ describe('cnft tests', () => { )}`, ); + console.log(`expectedBuyPrices: { + sellerReceives: ${expectedBuyPrices.sellerReceives.toString(10)}, + lpFeePaid: ${expectedBuyPrices.lpFeePaid.toString(10)}, + royaltyPaid: ${expectedBuyPrices.royaltyPaid.toString(10)}, + takerFeePaid: ${expectedBuyPrices.takerFeePaid.toString(10)}, + makerFeePaid: ${expectedBuyPrices.makerFeePaid.toString(10)} + }`); + const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), @@ -235,7 +243,7 @@ describe('cnft tests', () => { minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), allowlistAux: '', makerFeeBp: 0, - takerFeeBp: 0, + takerFeeBp: 100, creatorShares, creatorVerified, sellerFeeBasisPoints, From a31d1befbad11e62f48c10d072ece8a952a8cfdb Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Fri, 8 Nov 2024 17:49:59 -0800 Subject: [PATCH 19/35] pass basic test --- tests/mmm-cnft.spec.ts | 93 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index fcb093f..eeb327a 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -2,12 +2,16 @@ import * as anchor from '@project-serum/anchor'; import { isSome, publicKey, sol, Umi } from '@metaplex-foundation/umi'; import { airdrop, + assertIsBetween, createPool, createUmi, DEFAULT_TEST_SETUP_TREE_PARAMS, getCreatorRoyaltiesArgs, getPubKey, + getSellStatePDARent, + PRICE_ERROR_RANGE, setupTree, + SIGNATURE_FEE_LAMPORTS, verifyOwnership, } from './utils'; import { @@ -44,6 +48,7 @@ import { } from '@metaplex-foundation/mpl-bubblegum'; import { BN } from '@project-serum/anchor'; import { ConcurrentMerkleTreeAccount } from '@solana/spl-account-compression'; +import { assert } from 'chai'; async function createCNftCollectionOffer( program: anchor.Program, @@ -54,6 +59,7 @@ async function createCNftCollectionOffer( const poolData = await createPool(program, { ...poolArgs, reinvestFulfillBuy: false, + buysideCreatorRoyaltyBp: 10_000, }); const poolKey = poolData.poolKey; @@ -103,7 +109,7 @@ describe('cnft tests', () => { const buyer = new anchor.Wallet(Keypair.generate()); const seller = new anchor.Wallet(Keypair.generate()); const connection = new anchor.web3.Connection(endpoint, 'confirmed'); - const provider = new anchor.AnchorProvider(connection, buyer, { + let provider = new anchor.AnchorProvider(connection, buyer, { commitment: 'confirmed', }); @@ -176,7 +182,7 @@ describe('cnft tests', () => { const spotPrice = 1; const expectedBuyPrices = getSolFulfillBuyPrices({ totalPriceLamports: spotPrice * LAMPORTS_PER_SOL, - lpFeeBp: 200, + lpFeeBp: 0, takerFeeBp: 100, metadataRoyaltyBp: 500, buysideCreatorRoyaltyBp: 10_000, @@ -209,6 +215,29 @@ describe('cnft tests', () => { } = getCreatorRoyaltiesArgs(creatorRoyalties); console.log(`got creator royalties`); + // get balances before fulfill buy + const [ + buyerBefore, + sellerBefore, + buyerSolEscrowAccountBalanceBefore, + creator1Before, + creator2Before, + ] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buysideSolEscrowAccount), + connection.getBalance(creatorAccounts[0].pubkey), + connection.getBalance(creatorAccounts[1].pubkey), + ]); + + console.log(`buyerBefore: ${buyerBefore}`); + console.log(`sellerBefore: ${sellerBefore}`); + console.log( + `buyerSolEscrowAccountBalanceBefore: ${buyerSolEscrowAccountBalanceBefore}`, + ); + console.log(`creator1Before: ${creator1Before}`); + console.log(`creator2Before: ${creator2Before}`); + try { const metadataSerializer = getMetadataArgsSerializer(); const metadataArgs: MetadataArgs = metadataSerializer.deserialize( @@ -334,5 +363,65 @@ describe('cnft tests', () => { metadata, [], ); + + // Get balances after fulfill buy + const [ + buyerAfter, + sellerAfter, + buyerSolEscrowAccountBalanceAfter, + creator1After, + creator2After, + ] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buysideSolEscrowAccount), + connection.getBalance(creatorAccounts[0].pubkey), + connection.getBalance(creatorAccounts[1].pubkey), + ]); + + console.log(`buyerAfter: ${buyerAfter}`); + console.log(`sellerAfter: ${sellerAfter}`); + console.log( + `buyerSolEscrowAccountBalanceAfter: ${buyerSolEscrowAccountBalanceAfter}`, + ); + console.log(`creator1After: ${creator1After}`); + console.log(`creator2After: ${creator2After}`); + + const expectedTxFees = SIGNATURE_FEE_LAMPORTS * 3; // cosigner + seller + payer (due to provider is under buyer) + + assert.equal(buyerBefore, buyerAfter + expectedTxFees); + + assert.equal( + buyerSolEscrowAccountBalanceBefore, + buyerSolEscrowAccountBalanceAfter + spotPrice * LAMPORTS_PER_SOL, + ); + + // In production it should be seller buy tx fee, but with this test set up, buyer pays + // tx fee due to provider is initiated under buyer. + assert.equal( + sellerAfter, + sellerBefore + + spotPrice * LAMPORTS_PER_SOL - + expectedBuyPrices.takerFeePaid.toNumber() - + expectedBuyPrices.royaltyPaid.toNumber(), + ); + + assertIsBetween( + creator1After, + creator1Before + + (expectedBuyPrices.royaltyPaid.toNumber() * + metadata.creators[0].share) / + 100, + PRICE_ERROR_RANGE, + ); + + assertIsBetween( + creator2After, + creator2Before + + (expectedBuyPrices.royaltyPaid.toNumber() * + metadata.creators[1].share) / + 100, + PRICE_ERROR_RANGE, + ); }); }); From f4b528c189101e774aaf5c9d073388d638a1f338 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Fri, 8 Nov 2024 18:04:45 -0800 Subject: [PATCH 20/35] clean up redudent args and log --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 79 +++++++++---------- sdk/src/idl/mmm.ts | 32 -------- tests/mmm-cnft.spec.ts | 9 ++- 3 files changed, 43 insertions(+), 77 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 622298e..9b01bb1 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -49,12 +49,8 @@ pub struct SolCnftFulfillBuyArgs { pub maker_fee_bp: i16, // will be checked by cosigner pub taker_fee_bp: i16, // will be checked by cosigner - // === Creator args === // - creator_shares: Vec, - creator_verified: Vec, - // Creator royalties. Validated against the metadata_hash by Bubblegum after hashing with metadata_hash. - seller_fee_basis_points: u16, - + // Metadata args for cnft hash + // Reference: https://developers.metaplex.com/bubblegum/hashed-nft-data pub metadata_args: MetadataArgs, } @@ -153,26 +149,21 @@ pub fn handler<'info>( let payer = &ctx.accounts.payer; let referral: &UncheckedAccount<'info> = &ctx.accounts.referral; let merkle_tree = &ctx.accounts.merkle_tree; - // Remaining accounts are 1. (Optional) creator addresses and 2. Merkle proof path. - let creator_shares_length = args.creator_shares.len(); - let creator_shares_clone = args.creator_shares.clone(); - let remaining_accounts = ctx.remaining_accounts; - let system_program = &ctx.accounts.system_program; - - let data_hash = hash_metadata(&args.metadata_args)?; - let asset_mint = get_asset_id(&merkle_tree.key(), args.nonce); let pool_key = pool.key(); let buyside_sol_escrow_account_seeds: &[&[&[u8]]] = &[&[ BUYSIDE_SOL_ESCROW_ACCOUNT_PREFIX.as_bytes(), pool_key.as_ref(), &[ctx.bumps.buyside_sol_escrow_account], ]]; + let system_program = &ctx.accounts.system_program; + // Remaining accounts are 1. (Optional) creator addresses and 2. Merkle proof path. + let creator_length = args.metadata_args.creators.len(); + let remaining_accounts = ctx.remaining_accounts; - // 1. Cacluate seller receives + // 1. Cacluate amount and fees let (total_price, next_price) = get_sol_total_price_and_next_price(pool, args.asset_amount, true)?; let metadata_royalty_bp = args.metadata_args.seller_fee_basis_points; - // TODO: update lp_fee_bp when shared escrow for both side is enabled let seller_receives = { let lp_fee_bp = get_lp_fee_bp(pool, buyside_sol_escrow_account.lamports()); get_buyside_seller_receives( @@ -183,7 +174,6 @@ pub fn handler<'info>( ) }?; - // 2. Calculate fees let lp_fee = get_sol_lp_fee(pool, buyside_sol_escrow_account.lamports(), seller_receives)?; assert_valid_fees_bp(args.maker_fee_bp, args.taker_fee_bp)?; let maker_fee = get_sol_fee(seller_receives, args.maker_fee_bp)?; @@ -195,9 +185,7 @@ pub fn handler<'info>( ) .map_err(|_| MMMErrorCode::NumericOverflow)?; - // 3. Get creator accounts, verify creators - // check creator_accounts and verify the remaining accounts - // ... existing code ... + // 2. Get creator accounts, verify creators let (creator_accounts, proof_path) = if pool.using_shared_escrow() { check_remaining_accounts_for_m2(remaining_accounts, &pool.owner.key())?; @@ -217,18 +205,35 @@ pub fn handler<'info>( .checked_sub(args.asset_amount) .ok_or(MMMErrorCode::NumericOverflow)?; - remaining_accounts[2..].split_at(creator_shares_length + 2) + remaining_accounts[2..].split_at(creator_length + 2) } else { - remaining_accounts.split_at(creator_shares_length) + remaining_accounts.split_at(creator_length) }; + + let creator_shares = args + .metadata_args + .creators + .iter() + .map(|c| c.share as u16) + .collect::>(); + + let creator_verified = args + .metadata_args + .creators + .iter() + .map(|c| c.verified) + .collect(); + verify_creators( creator_accounts.iter(), - args.creator_shares, - args.creator_verified, + creator_shares, + creator_verified, args.creator_hash, )?; - // 4. Transfer CNFT to buyer (pool or owner) + // 3. Transfer CNFT to buyer (pool or owner) + let data_hash = hash_metadata(&args.metadata_args)?; + let asset_mint = get_asset_id(&merkle_tree.key(), args.nonce); if pool.reinvest_fulfill_buy { if pool.using_shared_escrow() { return Err(MMMErrorCode::InvalidAccountState.into()); @@ -284,7 +289,7 @@ pub fn handler<'info>( )?; } - // 5. Pool owner as buyer pay royalties to creators + // 4. Pool owner as buyer pay royalties to creators let royalty_paid = pay_creator_fees_in_sol_cnft( pool.buyside_creator_royalty_bp, seller_receives, @@ -295,9 +300,7 @@ pub fn handler<'info>( system_program.to_account_info(), )?; - // 6. Pay buyer, prevent frontrun by pool config changes - // the royalties are paid by the buyer, but the seller will see the price - // after adjusting the royalties. + // 5. Seller pay buyer, prevent frontrun by pool config changes let payment_amount = total_price .checked_sub(lp_fee) .ok_or(MMMErrorCode::NumericOverflow)? @@ -305,13 +308,6 @@ pub fn handler<'info>( .ok_or(MMMErrorCode::NumericOverflow)? .checked_sub(royalty_paid) .ok_or(MMMErrorCode::NumericOverflow)?; - msg!("payment_amount: {}", payment_amount); - msg!("args.min_payment_amount: {}", args.min_payment_amount); - msg!("total_price: {}", total_price); - msg!("lp_fee: {}", lp_fee); - msg!("taker_fee: {}", taker_fee); - msg!("royalty_paid: {}", royalty_paid); - msg!("seller_receives {}", seller_receives); if payment_amount < args.min_payment_amount { return Err(MMMErrorCode::InvalidRequestedPrice.into()); } @@ -330,7 +326,7 @@ pub fn handler<'info>( buyside_sol_escrow_account_seeds, )?; - // 7. pay lp fee + // 6. Pay lp fee if lp_fee > 0 { anchor_lang::solana_program::program::invoke_signed( &anchor_lang::solana_program::system_instruction::transfer( @@ -347,7 +343,7 @@ pub fn handler<'info>( )?; } - // 8. pay referral fee + // 7. Pay referral fee if referral_fee > 0 { anchor_lang::solana_program::program::invoke_signed( &anchor_lang::solana_program::system_instruction::transfer( @@ -364,7 +360,7 @@ pub fn handler<'info>( )?; } - // 9. try close buy side escrow account + // 8. try close accounts try_close_escrow( &buyside_sol_escrow_account.to_account_info(), pool, @@ -373,7 +369,7 @@ pub fn handler<'info>( )?; try_close_sell_state(sell_state, payer.to_account_info())?; - // 10. return the remaining per pool escrow balance to the shared escrow account + // 9. Return the remaining per pool escrow balance to the shared escrow account if pool.using_shared_escrow() { let min_rent = Rent::get()?.minimum_balance(0); let shared_escrow_account = index_ra!(remaining_accounts, 1).to_account_info(); @@ -402,7 +398,8 @@ pub fn handler<'info>( )?; } } - // 11. update pool state and log + + // 10. update pool state and log pool.lp_fee_earned = pool .lp_fee_earned .checked_add(lp_fee) diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 86bddf4..38e4888 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2740,22 +2740,6 @@ export type Mmm = { "name": "takerFeeBp", "type": "i16" }, - { - "name": "creatorShares", - "type": { - "vec": "u16" - } - }, - { - "name": "creatorVerified", - "type": { - "vec": "bool" - } - }, - { - "name": "sellerFeeBasisPoints", - "type": "u16" - }, { "name": "metadataArgs", "type": { @@ -6050,22 +6034,6 @@ export const IDL: Mmm = { "name": "takerFeeBp", "type": "i16" }, - { - "name": "creatorShares", - "type": { - "vec": "u16" - } - }, - { - "name": "creatorVerified", - "type": { - "vec": "bool" - } - }, - { - "name": "sellerFeeBasisPoints", - "type": "u16" - }, { "name": "metadataArgs", "type": { diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index eeb327a..93a2198 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -8,7 +8,6 @@ import { DEFAULT_TEST_SETUP_TREE_PARAMS, getCreatorRoyaltiesArgs, getPubKey, - getSellStatePDARent, PRICE_ERROR_RANGE, setupTree, SIGNATURE_FEE_LAMPORTS, @@ -273,9 +272,6 @@ describe('cnft tests', () => { allowlistAux: '', makerFeeBp: 0, takerFeeBp: 100, - creatorShares, - creatorVerified, - sellerFeeBasisPoints, metadataArgs: { name: metadataArgs.name, symbol: metadataArgs.symbol, @@ -424,4 +420,9 @@ describe('cnft tests', () => { PRICE_ERROR_RANGE, ); }); + + // TODO: Add test for + // 1. Wrong metadata args (like collection) + // 2. trucate canopy + // 3. reinvest = true }); From d176b467cdfa3ec72f3dac94f6e2fe8fa5f10334 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Mon, 11 Nov 2024 17:28:51 -0800 Subject: [PATCH 21/35] use pub! macro --- .../mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 9b01bb1..abb299f 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -1,7 +1,6 @@ -use std::str::FromStr; - use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; use mpl_bubblegum::utils::get_asset_id; +use solana_program::pubkey; use crate::{ constants::*, @@ -103,13 +102,14 @@ pub struct SolCnftFulfillBuy<'info> { #[account(mut)] merkle_tree: UncheckedAccount<'info>, /// CHECK: Used by bubblegum for logging (CPI) - #[account(address = Pubkey::from_str("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV").unwrap())] + #[account(address = pubkey!("noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"))] log_wrapper: UncheckedAccount<'info>, bubblegum_program: Program<'info, BubblegumProgram>, /// CHECK: The Solana Program Library spl-account-compression program ID. - #[account(address = Pubkey::from_str("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK").unwrap())] + #[account(address = pubkey!("cmtDvXumGCrqC1Age74AVPhSRVXJMd8PJS91L8KbNCK") +)] compression_program: UncheckedAccount<'info>, #[account( From 4afc5efd3ec1bc92bd78516c87bbcd1f6d45a8b1 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Mon, 11 Nov 2024 17:29:33 -0800 Subject: [PATCH 22/35] remove test skips --- tests/mmm-ext-fulfill.spec.ts | 2 +- tests/mmm-ocp.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/mmm-ext-fulfill.spec.ts b/tests/mmm-ext-fulfill.spec.ts index 6f8e951..f19710b 100644 --- a/tests/mmm-ext-fulfill.spec.ts +++ b/tests/mmm-ext-fulfill.spec.ts @@ -43,7 +43,7 @@ import { sendAndAssertTx, } from './utils'; -describe.skip('mmm-ext-fulfill', () => { +describe('mmm-ext-fulfill', () => { const { connection } = anchor.AnchorProvider.env(); const wallet = new anchor.Wallet(Keypair.generate()); const provider = new anchor.AnchorProvider(connection, wallet, { diff --git a/tests/mmm-ocp.spec.ts b/tests/mmm-ocp.spec.ts index 7d57480..c9b2714 100644 --- a/tests/mmm-ocp.spec.ts +++ b/tests/mmm-ocp.spec.ts @@ -42,7 +42,7 @@ import { PROGRAM_ID as OCP_PROGRAM_ID, } from '@magiceden-oss/open_creator_protocol'; -describe.skip('mmm-ocp', () => { +describe('mmm-ocp', () => { const { connection } = anchor.AnchorProvider.env(); const wallet = new anchor.Wallet(Keypair.generate()); const provider = new anchor.AnchorProvider(connection, wallet, { From dfeb37bc8737d51dce884e979579e01d38ef7ce4 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Mon, 11 Nov 2024 23:29:15 -0800 Subject: [PATCH 23/35] add allowlist verification --- programs/mmm/src/errors.rs | 8 ++-- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 34 +++++++------- programs/mmm/src/util.rs | 33 ++++++++++++- sdk/src/idl/mmm.ts | 46 ++++++------------- tests/mmm-cnft.spec.ts | 19 ++++---- tests/utils/cnft.ts | 39 ++++++++++++---- 6 files changed, 110 insertions(+), 69 deletions(-) diff --git a/programs/mmm/src/errors.rs b/programs/mmm/src/errors.rs index 829e634..f907950 100644 --- a/programs/mmm/src/errors.rs +++ b/programs/mmm/src/errors.rs @@ -72,8 +72,10 @@ pub enum MMMErrorCode { InvalidTokenExtension, // 0x1791 #[msg("Unsupported asset plugin")] UnsupportedAssetPlugin, // 0x1792 - #[msg("Mismatched ceator data lengths")] + #[msg("Mismatched creator data lengths")] MismatchedCreatorDataLengths, // 0x1793 - #[msg("Invalid creators")] - InvalidCreators, // 0x1794 + #[msg("Invalid cnft creators")] + InvalidCnftCreators, // 0x1794 + #[msg("Invalid cnft metadata")] + InvalidCnftMetadata, // 0x1795 } diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index abb299f..c9f6457 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -8,10 +8,11 @@ use crate::{ index_ra, state::{BubblegumProgram, Pool, SellState, TreeConfigAnchor}, util::{ - assert_valid_fees_bp, check_remaining_accounts_for_m2, get_buyside_seller_receives, - get_lp_fee_bp, get_sol_fee, get_sol_lp_fee, get_sol_total_price_and_next_price, - hash_metadata, log_pool, pay_creator_fees_in_sol_cnft, transfer_compressed_nft, - try_close_escrow, try_close_pool, try_close_sell_state, verify_creators, withdraw_m2, + assert_valid_fees_bp, check_allowlists_for_cnft, check_remaining_accounts_for_m2, + get_buyside_seller_receives, get_lp_fee_bp, get_sol_fee, get_sol_lp_fee, + get_sol_total_price_and_next_price, hash_metadata, log_pool, pay_creator_fees_in_sol_cnft, + transfer_compressed_nft, try_close_escrow, try_close_pool, try_close_sell_state, + verify_creators, withdraw_m2, }, verify_referral::verify_referral, }; @@ -39,14 +40,9 @@ pub struct SolCnftFulfillBuyArgs { // === Contract args === // // Price of the NFT in the payment_mint. buyer_price: u64, - // The mint of the SPL token used to pay for the NFT, currently not used and default to SOL. - payment_mint: Pubkey, - // The asset amount to deposit, default to 1. - pub asset_amount: u64, pub min_payment_amount: u64, - pub allowlist_aux: Option, // TODO: use it for future allowlist_aux - pub maker_fee_bp: i16, // will be checked by cosigner - pub taker_fee_bp: i16, // will be checked by cosigner + pub maker_fee_bp: i16, // will be checked by cosigner + pub taker_fee_bp: i16, // will be checked by cosigner // Metadata args for cnft hash // Reference: https://developers.metaplex.com/bubblegum/hashed-nft-data @@ -160,9 +156,15 @@ pub fn handler<'info>( let creator_length = args.metadata_args.creators.len(); let remaining_accounts = ctx.remaining_accounts; + // 0. Verify allowlist + if let Some(ref collection) = args.metadata_args.collection { + let _ = check_allowlists_for_cnft(&pool.allowlists, collection.clone()); + } else { + return Err(MMMErrorCode::InvalidCnftMetadata.into()); + } + // 1. Cacluate amount and fees - let (total_price, next_price) = - get_sol_total_price_and_next_price(pool, args.asset_amount, true)?; + let (total_price, next_price) = get_sol_total_price_and_next_price(pool, 1, true)?; let metadata_royalty_bp = args.metadata_args.seller_fee_basis_points; let seller_receives = { let lp_fee_bp = get_lp_fee_bp(pool, buyside_sol_escrow_account.lamports()); @@ -202,7 +204,7 @@ pub fn handler<'info>( )?; pool.shared_escrow_count = pool .shared_escrow_count - .checked_sub(args.asset_amount) + .checked_sub(1) .ok_or(MMMErrorCode::NumericOverflow)?; remaining_accounts[2..].split_at(creator_length + 2) @@ -258,7 +260,7 @@ pub fn handler<'info>( )?; pool.sellside_asset_amount = pool .sellside_asset_amount - .checked_add(args.asset_amount) + .checked_add(1) .ok_or(MMMErrorCode::NumericOverflow)?; sell_state.pool = pool.key(); sell_state.pool_owner = owner.key(); @@ -266,7 +268,7 @@ pub fn handler<'info>( sell_state.cosigner_annotation = pool.cosigner_annotation; sell_state.asset_amount = sell_state .asset_amount - .checked_add(args.asset_amount) + .checked_add(1) .ok_or(MMMErrorCode::NumericOverflow)?; } else { transfer_compressed_nft( diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index a577903..8eeac3c 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -7,7 +7,7 @@ use crate::{ errors::MMMErrorCode, get_creators_from_royalties, state::*, - IndexableAsset, MetadataArgs, + Collection, IndexableAsset, MetadataArgs, }; use anchor_lang::{prelude::*, solana_program::log::sol_log_data}; use anchor_spl::token_interface::Mint; @@ -157,6 +157,35 @@ pub fn check_allowlists_for_mint( Err(MMMErrorCode::InvalidAllowLists.into()) } +pub fn check_allowlists_for_cnft(allowlists: &[Allowlist], collection: Collection) -> Result<()> { + // We need to check the following validation rules + // 1. make sure the metadata is correctly derived from the metadata pda with the mint + // 2. make sure mint+metadata(e.g. first verified creator address) can match one of the allowlist + // 3. note that the allowlist is unioned together, not intersection + // 4. skip if the allowlist.is_empty() + // 5. verify that nft either does not have master edition or is master edition + for allowlist_val in allowlists.iter() { + match allowlist_val.kind { + ALLOWLIST_KIND_EMPTY => {} + ALLOWLIST_KIND_ANY => { + // any is a special case, we don't need to check anything else + return Ok(()); + } + ALLOWLIST_KIND_MCC => { + if collection.key == allowlist_val.value && collection.verified { + return Ok(()); + } + } + _ => { + return Err(MMMErrorCode::InvalidAllowLists.into()); + } + } + } + + // at the end, we didn't find a match, thus return err + Err(MMMErrorCode::InvalidAllowLists.into()) +} + pub fn check_curve(curve_type: u8, curve_delta: u64) -> Result<()> { // So far we only allow linear and exponential curves // 0: linear @@ -1342,7 +1371,7 @@ pub fn verify_creators( computed_hash, creator_hash ); - return Err(MMMErrorCode::InvalidCreators.into()); + return Err(MMMErrorCode::InvalidCnftCreators.into()); } Ok(()) diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 38e4888..cea48f3 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2714,24 +2714,10 @@ export type Mmm = { "name": "buyerPrice", "type": "u64" }, - { - "name": "paymentMint", - "type": "publicKey" - }, - { - "name": "assetAmount", - "type": "u64" - }, { "name": "minPaymentAmount", "type": "u64" }, - { - "name": "allowlistAux", - "type": { - "option": "string" - } - }, { "name": "makerFeeBp", "type": "i16" @@ -3286,8 +3272,13 @@ export type Mmm = { }, { "code": 6036, - "name": "InvalidCreators", - "msg": "Invalid creators" + "name": "InvalidCnftCreators", + "msg": "Invalid cnft creators" + }, + { + "code": 6037, + "name": "InvalidCnftMetadata", + "msg": "Invalid cnft metadata" } ] }; @@ -6008,24 +5999,10 @@ export const IDL: Mmm = { "name": "buyerPrice", "type": "u64" }, - { - "name": "paymentMint", - "type": "publicKey" - }, - { - "name": "assetAmount", - "type": "u64" - }, { "name": "minPaymentAmount", "type": "u64" }, - { - "name": "allowlistAux", - "type": { - "option": "string" - } - }, { "name": "makerFeeBp", "type": "i16" @@ -6580,8 +6557,13 @@ export const IDL: Mmm = { }, { "code": 6036, - "name": "InvalidCreators", - "msg": "Invalid creators" + "name": "InvalidCnftCreators", + "msg": "Invalid cnft creators" + }, + { + "code": 6037, + "name": "InvalidCnftMetadata", + "msg": "Invalid cnft metadata" } ] }; diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 93a2198..787991e 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -7,6 +7,7 @@ import { createUmi, DEFAULT_TEST_SETUP_TREE_PARAMS, getCreatorRoyaltiesArgs, + getEmptyAllowLists, getPubKey, PRICE_ERROR_RANGE, setupTree, @@ -14,6 +15,7 @@ import { verifyOwnership, } from './utils'; import { + AllowlistKind, convertToDecodeTokenProgramVersion, convertToDecodeTokenStandardEnum, convertToDecodeUseMethodEnum, @@ -101,8 +103,6 @@ async function createCNftCollectionOffer( }; } -const SOL = new PublicKey('So11111111111111111111111111111111111111112'); - describe('cnft tests', () => { const endpoint = 'http://localhost:8899'; const buyer = new anchor.Wallet(Keypair.generate()); @@ -127,9 +127,7 @@ describe('cnft tests', () => { airdrop(connection, cosigner.publicKey, 100); }); - it.only('cnft fulfill buy', async () => { - const umi = await createUmi(endpoint, sol(3)); - + it('cnft fulfill buy - happy path', async () => { console.log(`buyer: ${buyer.publicKey}`); console.log(`seller: ${seller.publicKey}`); // 1. Create a tree. @@ -142,6 +140,7 @@ describe('cnft tests', () => { getCnftRef, nft, creatorRoyalties, + collectionKey, } = await setupTree( umi, publicKey(seller.publicKey), @@ -155,6 +154,13 @@ describe('cnft tests', () => { await createCNftCollectionOffer(program, { owner: new PublicKey(buyer.publicKey), cosigner, + allowlists: [ + { + kind: AllowlistKind.mcc, + value: collectionKey, + }, + ...getEmptyAllowLists(5), + ], }); const [treeAuthority, _] = getBubblegumAuthorityPDA( @@ -266,10 +272,7 @@ describe('cnft tests', () => { nonce: new BN(nft.tree.nonce), index: nft.nft.nftIndex, buyerPrice: new BN(spotPrice * LAMPORTS_PER_SOL), - paymentMint: SOL, - assetAmount: new BN(1), minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), - allowlistAux: '', makerFeeBp: 0, takerFeeBp: 100, metadataArgs: { diff --git a/tests/utils/cnft.ts b/tests/utils/cnft.ts index 082db9c..ef28d8d 100644 --- a/tests/utils/cnft.ts +++ b/tests/utils/cnft.ts @@ -16,6 +16,7 @@ import { getMerkleProof, verifyLeaf, MerkleTree, + MPL_BUBBLEGUM_PROGRAM_ID, } from '@metaplex-foundation/mpl-bubblegum'; import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; import { @@ -33,8 +34,14 @@ import { } from '@metaplex-foundation/umi'; import { createUmi as baseCreateUmi } from '@metaplex-foundation/umi-bundle-tests'; import { BubblegumTreeRef, CNFT, CreatorRoyaltyConfig } from '../../sdk/src'; -import { AccountMeta, PublicKey as Web3PubKey } from '@solana/web3.js'; +import { + AccountMeta, + Connection, + PublicKey as Web3PubKey, +} from '@solana/web3.js'; import { dasApi } from '@metaplex-foundation/digital-asset-standard-api'; +import { mintCollection } from './nfts'; +import { umiMintCollection } from './umiNfts'; export const ME_TREASURY = new Web3PubKey( 'rFqFJ9g7TGBD8Ed7TPDnvGKZ5pWLPDyxLcvcH2eRCtt', @@ -95,6 +102,7 @@ export const mint = async ( metadata?: Partial[1]['metadata']>; leafOwner?: PublicKey; creators?: Parameters[1]['metadata']['creators']; + collection?: Parameters[1]['metadata']['collection']; }, ): Promise<{ metadata: MetadataArgsArgs; @@ -110,11 +118,12 @@ export const mint = async ( (await fetchMerkleTree(context, merkleTree)).tree.activeIndex, ); const leafCreators = input.creators ?? []; + const collection = input.collection ?? none(); const metadata: MetadataArgsArgs = { name: 'My NFT', uri: 'https://example.com/my-nft.json', sellerFeeBasisPoints: 500, // 5% - collection: none(), + collection, creators: leafCreators, ...input.metadata, }; @@ -234,17 +243,28 @@ export async function setupTree( const creatorSigners = await getCreatorPair(umi); const unverifiedCreators = await initUnverifiedCreatorsArray(creatorSigners); + const collection = ( + await umiMintCollection( + umi, + { + numNfts: 0, + verifyCollection: false, + legacy: true, + }, + new Web3PubKey(MPL_BUBBLEGUM_PROGRAM_ID), + ) + ).collection; + const { metadata, leaf, leafIndex, creatorsHash, assetId } = await mint(umi, { merkleTree, leafOwner: seller, creators: unverifiedCreators, + collection: { + key: publicKey(collection.mintAddress), + verified: false, + }, }); - console.log(`merkleTree: ${merkleTree}`); - console.log(`leaf: ${leaf}`); - console.log(`leafIndex: ${leafIndex}`); - console.log(`assetId: ${assetId}`); - const verifyCreatorProofTruncated = getTruncatedMerkleProof( treeParams.canopyDepth, [leaf], @@ -320,7 +340,9 @@ export async function setupTree( console.log(` [setupTree] fullProof(length: ${fullProof.length}): ${JSON.stringify(fullProof)} - sellerProof[truncated](length: ${sellerProof.length}): ${JSON.stringify(sellerProof)} + sellerProof[truncated](length: ${sellerProof.length}): ${JSON.stringify( + sellerProof, + )} `); return { merkleTree, @@ -343,6 +365,7 @@ export async function setupTree( })), sellerFeeBasisPoints: 500, // 5% royalty }, + collectionKey: new Web3PubKey(collection.mintAddress), }; } From 460929ef6f84443961e59d8fdcf151ef52e2912d Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Tue, 12 Nov 2024 00:30:08 -0800 Subject: [PATCH 24/35] propgate error --- programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs | 2 +- sdk/src/idl/mmm.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index c9f6457..a5540be 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -158,7 +158,7 @@ pub fn handler<'info>( // 0. Verify allowlist if let Some(ref collection) = args.metadata_args.collection { - let _ = check_allowlists_for_cnft(&pool.allowlists, collection.clone()); + let _ = check_allowlists_for_cnft(&pool.allowlists, collection.clone())?; } else { return Err(MMMErrorCode::InvalidCnftMetadata.into()); } diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index cea48f3..33fa9dc 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -3268,7 +3268,7 @@ export type Mmm = { { "code": 6035, "name": "MismatchedCreatorDataLengths", - "msg": "Mismatched ceator data lengths" + "msg": "Mismatched creator data lengths" }, { "code": 6036, @@ -6553,7 +6553,7 @@ export const IDL: Mmm = { { "code": 6035, "name": "MismatchedCreatorDataLengths", - "msg": "Mismatched ceator data lengths" + "msg": "Mismatched creator data lengths" }, { "code": 6036, From bfca62f00d35f4b20ae168f0e426d63205212485 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 13 Nov 2024 11:07:32 -0800 Subject: [PATCH 25/35] fix collection verification for mcc --- tests/mmm-cnft.spec.ts | 296 +++++++++++++++++++++++++++++++++++++++++ tests/utils/cnft.ts | 71 ++++++---- 2 files changed, 342 insertions(+), 25 deletions(-) diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 787991e..bc3e0a8 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -428,4 +428,300 @@ describe('cnft tests', () => { // 1. Wrong metadata args (like collection) // 2. trucate canopy // 3. reinvest = true + it('cnft fulfill buy - incorrect collection fail allowlist check', async () => { + console.log(`buyer: ${buyer.publicKey}`); + console.log(`seller: ${seller.publicKey}`); + // 1. Create a tree. + const { + merkleTree, + sellerProof, //already truncated + leafIndex, + metadata, + getBubblegumTreeRef, + getCnftRef, + nft, + creatorRoyalties, + collectionKey + } = await setupTree( + umi, + publicKey(seller.publicKey), + DEFAULT_TEST_SETUP_TREE_PARAMS, + ); + + const merkleyTreePubkey = getPubKey(merkleTree); + + // 2. Create an offer. + const { buysideSolEscrowAccount, poolData } = + await createCNftCollectionOffer(program, { + owner: new PublicKey(buyer.publicKey), + cosigner, + allowlists: [ + { + kind: AllowlistKind.mcc, + value: collectionKey, + }, + ...getEmptyAllowLists(5), + ], + }); + + const [treeAuthority, _] = getBubblegumAuthorityPDA( + new PublicKey(nft.tree.merkleTree), + ); + + const [assetId, bump] = findLeafAssetIdPda(umi, { + merkleTree, + leafIndex, + }); + + // const asset = await umi.rpc.getAsset(assetId); + // console.log(`asset: ${JSON.stringify(asset)}`); + // const assetWithProof = await getAssetWithProof(umi, assetId); + // console.log(`assetWithProof: ${JSON.stringify(assetWithProof)}`); + + const { key: sellState } = getMMMCnftSellStatePDA( + program.programId, + poolData.poolKey, + new PublicKey(nft.tree.merkleTree), + nft.nft.nftIndex, + ); + + const spotPrice = 1; + const expectedBuyPrices = getSolFulfillBuyPrices({ + totalPriceLamports: spotPrice * LAMPORTS_PER_SOL, + lpFeeBp: 0, + takerFeeBp: 100, + metadataRoyaltyBp: 500, + buysideCreatorRoyaltyBp: 10_000, + makerFeeBp: 0, + }); + + const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress( + connection, + nft.tree.merkleTree, + ); + + console.log(`merkleTree: ${nft.tree.merkleTree}`); + console.log(`proofs: ${nft.nft.fullProof}`); + console.log(`canopyDepth: ${treeAccount.getCanopyDepth()}`); + + const proofPath: AccountMeta[] = getProofPath( + nft.nft.fullProof, + treeAccount.getCanopyDepth(), + ); + console.log(`nft.nft.proofs.length: ${nft.nft.fullProof.length}`); + console.log(`proofPath.length: ${proofPath.length}`); + + console.log(`proofPath: ${JSON.stringify(proofPath)}`); + + const { + accounts: creatorAccounts, + creatorShares, + creatorVerified, + sellerFeeBasisPoints, + } = getCreatorRoyaltiesArgs(creatorRoyalties); + console.log(`got creator royalties`); + + // get balances before fulfill buy + const [ + buyerBefore, + sellerBefore, + buyerSolEscrowAccountBalanceBefore, + creator1Before, + creator2Before, + ] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buysideSolEscrowAccount), + connection.getBalance(creatorAccounts[0].pubkey), + connection.getBalance(creatorAccounts[1].pubkey), + ]); + + console.log(`buyerBefore: ${buyerBefore}`); + console.log(`sellerBefore: ${sellerBefore}`); + console.log( + `buyerSolEscrowAccountBalanceBefore: ${buyerSolEscrowAccountBalanceBefore}`, + ); + console.log(`creator1Before: ${creator1Before}`); + console.log(`creator2Before: ${creator2Before}`); + + try { + const metadataSerializer = getMetadataArgsSerializer(); + const metadataArgs: MetadataArgs = metadataSerializer.deserialize( + metadataSerializer.serialize(metadata), + )[0]; + + console.log(`metadataArgs: ${JSON.stringify(metadataArgs)}`); + console.log( + `${JSON.stringify( + convertToDecodeTokenProgramVersion(metadataArgs.tokenProgramVersion), + )}`, + ); + + console.log(`expectedBuyPrices: { + sellerReceives: ${expectedBuyPrices.sellerReceives.toString(10)}, + lpFeePaid: ${expectedBuyPrices.lpFeePaid.toString(10)}, + royaltyPaid: ${expectedBuyPrices.royaltyPaid.toString(10)}, + takerFeePaid: ${expectedBuyPrices.takerFeePaid.toString(10)}, + makerFeePaid: ${expectedBuyPrices.makerFeePaid.toString(10)} + }`); + + const fulfillBuyTxnSig = await program.methods + .cnftFulfillBuy({ + root: getByteArray(nft.tree.root), + metadataHash: getByteArray(nft.tree.dataHash), + creatorHash: getByteArray(nft.tree.creatorHash), + nonce: new BN(nft.tree.nonce), + index: nft.nft.nftIndex, + buyerPrice: new BN(spotPrice * LAMPORTS_PER_SOL), + minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), + makerFeeBp: 0, + takerFeeBp: 100, + metadataArgs: { + name: metadataArgs.name, + symbol: metadataArgs.symbol, + uri: metadataArgs.uri, + sellerFeeBasisPoints: metadataArgs.sellerFeeBasisPoints, + primarySaleHappened: metadataArgs.primarySaleHappened, + isMutable: metadataArgs.isMutable, + editionNonce: isSome(metadataArgs.editionNonce) + ? metadataArgs.editionNonce.value + : null, + tokenStandard: isSome(metadataArgs.tokenStandard) + ? convertToDecodeTokenStandardEnum( + metadataArgs.tokenStandard.value, + ) + : null, + collection: isSome(metadataArgs.collection) + ? { + verified: metadataArgs.collection.value.verified, + key: SystemProgram.programId, // !!!! WRONG COLLECTION + } + : null, // Ensure it's a struct or null + uses: isSome(metadataArgs.uses) + ? { + useMethod: convertToDecodeUseMethodEnum( + metadataArgs.uses.value.useMethod, + ), + remaining: metadataArgs.uses.value.remaining, + total: metadataArgs.uses.value.total, + } + : null, + tokenProgramVersion: convertToDecodeTokenProgramVersion( + metadataArgs.tokenProgramVersion, + ), + creators: metadataArgs.creators.map((c) => ({ + address: new PublicKey(c.address), + verified: c.verified, + share: c.share, + })), + }, + }) + .accountsStrict({ + payer: new PublicKey(seller.publicKey), + owner: buyer.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount, + treeAuthority, + merkleTree: nft.tree.merkleTree, + logWrapper: SPL_NOOP_PROGRAM_ID, + bubblegumProgram: MPL_BUBBLEGUM_PROGRAM_ID, + compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, + sellState, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts([...creatorAccounts, ...proofPath]) + .signers([cosigner, seller.payer]) + // note: skipPreflight causes some weird error. + // so just surround in this try-catch to get the logs + .rpc(/* { skipPreflight: true } */); + console.log(`fulfillBuyTxnSig: ${fulfillBuyTxnSig}`); + } catch (e) { + if (e instanceof SendTransactionError) { + const err = e as SendTransactionError; + console.log( + `err.logs: ${JSON.stringify( + await err.getLogs(provider.connection), + null, + 2, + )}`, + ); + } + throw e; + } + + console.log(`seller: ${seller.publicKey}`); + console.log(`buyer: ${buyer.publicKey}`); + console.log(`nft: ${JSON.stringify(nft)}`); + // Verify that buyer now owns the cNFT. + await verifyOwnership( + umi, + merkleTree, + publicKey(buyer.publicKey), + leafIndex, + metadata, + [], + ); + + // Get balances after fulfill buy + const [ + buyerAfter, + sellerAfter, + buyerSolEscrowAccountBalanceAfter, + creator1After, + creator2After, + ] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buysideSolEscrowAccount), + connection.getBalance(creatorAccounts[0].pubkey), + connection.getBalance(creatorAccounts[1].pubkey), + ]); + + console.log(`buyerAfter: ${buyerAfter}`); + console.log(`sellerAfter: ${sellerAfter}`); + console.log( + `buyerSolEscrowAccountBalanceAfter: ${buyerSolEscrowAccountBalanceAfter}`, + ); + console.log(`creator1After: ${creator1After}`); + console.log(`creator2After: ${creator2After}`); + + const expectedTxFees = SIGNATURE_FEE_LAMPORTS * 3; // cosigner + seller + payer (due to provider is under buyer) + + assert.equal(buyerBefore, buyerAfter + expectedTxFees); + + assert.equal( + buyerSolEscrowAccountBalanceBefore, + buyerSolEscrowAccountBalanceAfter + spotPrice * LAMPORTS_PER_SOL, + ); + + // In production it should be seller buy tx fee, but with this test set up, buyer pays + // tx fee due to provider is initiated under buyer. + assert.equal( + sellerAfter, + sellerBefore + + spotPrice * LAMPORTS_PER_SOL - + expectedBuyPrices.takerFeePaid.toNumber() - + expectedBuyPrices.royaltyPaid.toNumber(), + ); + + assertIsBetween( + creator1After, + creator1Before + + (expectedBuyPrices.royaltyPaid.toNumber() * + metadata.creators[0].share) / + 100, + PRICE_ERROR_RANGE, + ); + + assertIsBetween( + creator2After, + creator2Before + + (expectedBuyPrices.royaltyPaid.toNumber() * + metadata.creators[1].share) / + 100, + PRICE_ERROR_RANGE, + ); + }); }); diff --git a/tests/utils/cnft.ts b/tests/utils/cnft.ts index ef28d8d..377de42 100644 --- a/tests/utils/cnft.ts +++ b/tests/utils/cnft.ts @@ -16,15 +16,18 @@ import { getMerkleProof, verifyLeaf, MerkleTree, - MPL_BUBBLEGUM_PROGRAM_ID, + mintToCollectionV1, } from '@metaplex-foundation/mpl-bubblegum'; -import { mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; +import { createNft, mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; import { Context, generateSigner, + isOption, + isSome, KeypairSigner, none, Pda, + percentAmount, PublicKey, publicKey, sol, @@ -40,8 +43,6 @@ import { PublicKey as Web3PubKey, } from '@solana/web3.js'; import { dasApi } from '@metaplex-foundation/digital-asset-standard-api'; -import { mintCollection } from './nfts'; -import { umiMintCollection } from './umiNfts'; export const ME_TREASURY = new Web3PubKey( 'rFqFJ9g7TGBD8Ed7TPDnvGKZ5pWLPDyxLcvcH2eRCtt', @@ -118,21 +119,42 @@ export const mint = async ( (await fetchMerkleTree(context, merkleTree)).tree.activeIndex, ); const leafCreators = input.creators ?? []; - const collection = input.collection ?? none(); + const collection = input.collection + ? isOption(input.collection) + ? isSome(input.collection) + ? input.collection.value + : undefined + : input.collection + : undefined; + const metadata: MetadataArgsArgs = { name: 'My NFT', uri: 'https://example.com/my-nft.json', sellerFeeBasisPoints: 500, // 5% - collection, + collection: collection + ? { + key: collection.key, + verified: collection.verified, + } + : none(), creators: leafCreators, ...input.metadata, }; - await baseMintV1(context, { - ...input, - metadata, - leafOwner, - }).sendAndConfirm(context); + if (collection) { + await mintToCollectionV1(context, { + ...input, + leafOwner, + collectionMint: collection.key, + metadata, + }).sendAndConfirm(context); + } else { + await baseMintV1(context, { + ...input, + metadata, + leafOwner, + }).sendAndConfirm(context); + } return { metadata, @@ -243,28 +265,27 @@ export async function setupTree( const creatorSigners = await getCreatorPair(umi); const unverifiedCreators = await initUnverifiedCreatorsArray(creatorSigners); - const collection = ( - await umiMintCollection( - umi, - { - numNfts: 0, - verifyCollection: false, - legacy: true, - }, - new Web3PubKey(MPL_BUBBLEGUM_PROGRAM_ID), - ) - ).collection; + const collectionMint = generateSigner(umi) + await createNft(umi, { + mint: collectionMint, + name: 'My Collection', + uri: 'https://example.com/my-collection.json', + sellerFeeBasisPoints: percentAmount(5), // % + isCollection: true, + }).sendAndConfirm(umi); const { metadata, leaf, leafIndex, creatorsHash, assetId } = await mint(umi, { merkleTree, leafOwner: seller, creators: unverifiedCreators, collection: { - key: publicKey(collection.mintAddress), - verified: false, + key: publicKey(collectionMint), + verified: true, }, }); + console.log(`metadata: ${JSON.stringify(metadata)}`); + const verifyCreatorProofTruncated = getTruncatedMerkleProof( treeParams.canopyDepth, [leaf], @@ -365,7 +386,7 @@ export async function setupTree( })), sellerFeeBasisPoints: 500, // 5% royalty }, - collectionKey: new Web3PubKey(collection.mintAddress), + collectionKey: new Web3PubKey(collectionMint.publicKey), }; } From affe6112a061142009eeec177e25f5151db4ffcc Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 13 Nov 2024 11:41:49 -0800 Subject: [PATCH 26/35] fix allowlist check test --- tests/mmm-cnft.spec.ts | 56 +++++++++++------------------------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index bc3e0a8..e5c03ee 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -441,7 +441,7 @@ describe('cnft tests', () => { getCnftRef, nft, creatorRoyalties, - collectionKey + collectionKey, } = await setupTree( umi, publicKey(seller.publicKey), @@ -638,27 +638,23 @@ describe('cnft tests', () => { .rpc(/* { skipPreflight: true } */); console.log(`fulfillBuyTxnSig: ${fulfillBuyTxnSig}`); } catch (e) { - if (e instanceof SendTransactionError) { - const err = e as SendTransactionError; - console.log( - `err.logs: ${JSON.stringify( - await err.getLogs(provider.connection), - null, - 2, - )}`, - ); - } - throw e; + expect(e).toBeInstanceOf(anchor.AnchorError); + const err = e as anchor.AnchorError; + + assert.strictEqual( + err.message, + 'AnchorError occurred. Error Code: InvalidAllowLists. Error Number: 6001. Error Message: invalid allowlists.', + ); } console.log(`seller: ${seller.publicKey}`); console.log(`buyer: ${buyer.publicKey}`); console.log(`nft: ${JSON.stringify(nft)}`); - // Verify that buyer now owns the cNFT. + // Verify that seller still owns the cNFT. await verifyOwnership( umi, merkleTree, - publicKey(buyer.publicKey), + publicKey(seller.publicKey), leafIndex, metadata, [], @@ -687,41 +683,19 @@ describe('cnft tests', () => { console.log(`creator1After: ${creator1After}`); console.log(`creator2After: ${creator2After}`); - const expectedTxFees = SIGNATURE_FEE_LAMPORTS * 3; // cosigner + seller + payer (due to provider is under buyer) - - assert.equal(buyerBefore, buyerAfter + expectedTxFees); + assert.equal(buyerBefore, buyerAfter); assert.equal( buyerSolEscrowAccountBalanceBefore, - buyerSolEscrowAccountBalanceAfter + spotPrice * LAMPORTS_PER_SOL, + buyerSolEscrowAccountBalanceAfter, ); // In production it should be seller buy tx fee, but with this test set up, buyer pays // tx fee due to provider is initiated under buyer. - assert.equal( - sellerAfter, - sellerBefore + - spotPrice * LAMPORTS_PER_SOL - - expectedBuyPrices.takerFeePaid.toNumber() - - expectedBuyPrices.royaltyPaid.toNumber(), - ); + assert.equal(sellerAfter, sellerBefore); - assertIsBetween( - creator1After, - creator1Before + - (expectedBuyPrices.royaltyPaid.toNumber() * - metadata.creators[0].share) / - 100, - PRICE_ERROR_RANGE, - ); + assert.equal(creator1After, creator1Before); - assertIsBetween( - creator2After, - creator2Before + - (expectedBuyPrices.royaltyPaid.toNumber() * - metadata.creators[1].share) / - 100, - PRICE_ERROR_RANGE, - ); + assert.equal(creator2After, creator2Before); }); }); From d7955f158445dbbefa9b2b1696877ac0b4785f4d Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 13 Nov 2024 12:02:42 -0800 Subject: [PATCH 27/35] add incorrect royalty test --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 2 + tests/mmm-cnft.spec.ts | 273 +++++++++++++++++- 2 files changed, 274 insertions(+), 1 deletion(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index a5540be..db8614f 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -236,6 +236,8 @@ pub fn handler<'info>( // 3. Transfer CNFT to buyer (pool or owner) let data_hash = hash_metadata(&args.metadata_args)?; let asset_mint = get_asset_id(&merkle_tree.key(), args.nonce); + // reinvest fulfill buy is just a placeholder for now if we want to enable double sided + // pool for cnft in the the future. if pool.reinvest_fulfill_buy { if pool.using_shared_escrow() { return Err(MMMErrorCode::InvalidAccountState.into()); diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index e5c03ee..e9b3d04 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -427,7 +427,6 @@ describe('cnft tests', () => { // TODO: Add test for // 1. Wrong metadata args (like collection) // 2. trucate canopy - // 3. reinvest = true it('cnft fulfill buy - incorrect collection fail allowlist check', async () => { console.log(`buyer: ${buyer.publicKey}`); console.log(`seller: ${seller.publicKey}`); @@ -698,4 +697,276 @@ describe('cnft tests', () => { assert.equal(creator2After, creator2Before); }); + + it('cnft fulfill buy - incorrect royalty fail bubblegum check', async () => { + console.log(`buyer: ${buyer.publicKey}`); + console.log(`seller: ${seller.publicKey}`); + // 1. Create a tree. + const { + merkleTree, + sellerProof, //already truncated + leafIndex, + metadata, + getBubblegumTreeRef, + getCnftRef, + nft, + creatorRoyalties, + collectionKey, + } = await setupTree( + umi, + publicKey(seller.publicKey), + DEFAULT_TEST_SETUP_TREE_PARAMS, + ); + + const merkleyTreePubkey = getPubKey(merkleTree); + + // 2. Create an offer. + const { buysideSolEscrowAccount, poolData } = + await createCNftCollectionOffer(program, { + owner: new PublicKey(buyer.publicKey), + cosigner, + allowlists: [ + { + kind: AllowlistKind.mcc, + value: collectionKey, + }, + ...getEmptyAllowLists(5), + ], + }); + + const [treeAuthority, _] = getBubblegumAuthorityPDA( + new PublicKey(nft.tree.merkleTree), + ); + + const [assetId, bump] = findLeafAssetIdPda(umi, { + merkleTree, + leafIndex, + }); + + // const asset = await umi.rpc.getAsset(assetId); + // console.log(`asset: ${JSON.stringify(asset)}`); + // const assetWithProof = await getAssetWithProof(umi, assetId); + // console.log(`assetWithProof: ${JSON.stringify(assetWithProof)}`); + + const { key: sellState } = getMMMCnftSellStatePDA( + program.programId, + poolData.poolKey, + new PublicKey(nft.tree.merkleTree), + nft.nft.nftIndex, + ); + + const spotPrice = 1; + const expectedBuyPrices = getSolFulfillBuyPrices({ + totalPriceLamports: spotPrice * LAMPORTS_PER_SOL, + lpFeeBp: 0, + takerFeeBp: 100, + metadataRoyaltyBp: 500, + buysideCreatorRoyaltyBp: 10_000, + makerFeeBp: 0, + }); + + const treeAccount = await ConcurrentMerkleTreeAccount.fromAccountAddress( + connection, + nft.tree.merkleTree, + ); + + console.log(`merkleTree: ${nft.tree.merkleTree}`); + console.log(`proofs: ${nft.nft.fullProof}`); + console.log(`canopyDepth: ${treeAccount.getCanopyDepth()}`); + + const proofPath: AccountMeta[] = getProofPath( + nft.nft.fullProof, + treeAccount.getCanopyDepth(), + ); + console.log(`nft.nft.proofs.length: ${nft.nft.fullProof.length}`); + console.log(`proofPath.length: ${proofPath.length}`); + + console.log(`proofPath: ${JSON.stringify(proofPath)}`); + + const { + accounts: creatorAccounts, + creatorShares, + creatorVerified, + sellerFeeBasisPoints, + } = getCreatorRoyaltiesArgs(creatorRoyalties); + console.log(`got creator royalties`); + + // get balances before fulfill buy + const [ + buyerBefore, + sellerBefore, + buyerSolEscrowAccountBalanceBefore, + creator1Before, + creator2Before, + ] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buysideSolEscrowAccount), + connection.getBalance(creatorAccounts[0].pubkey), + connection.getBalance(creatorAccounts[1].pubkey), + ]); + + console.log(`buyerBefore: ${buyerBefore}`); + console.log(`sellerBefore: ${sellerBefore}`); + console.log( + `buyerSolEscrowAccountBalanceBefore: ${buyerSolEscrowAccountBalanceBefore}`, + ); + console.log(`creator1Before: ${creator1Before}`); + console.log(`creator2Before: ${creator2Before}`); + + try { + const metadataSerializer = getMetadataArgsSerializer(); + const metadataArgs: MetadataArgs = metadataSerializer.deserialize( + metadataSerializer.serialize(metadata), + )[0]; + + console.log(`metadataArgs: ${JSON.stringify(metadataArgs)}`); + console.log( + `${JSON.stringify( + convertToDecodeTokenProgramVersion(metadataArgs.tokenProgramVersion), + )}`, + ); + + console.log(`expectedBuyPrices: { + sellerReceives: ${expectedBuyPrices.sellerReceives.toString(10)}, + lpFeePaid: ${expectedBuyPrices.lpFeePaid.toString(10)}, + royaltyPaid: ${expectedBuyPrices.royaltyPaid.toString(10)}, + takerFeePaid: ${expectedBuyPrices.takerFeePaid.toString(10)}, + makerFeePaid: ${expectedBuyPrices.makerFeePaid.toString(10)} + }`); + + const fulfillBuyTxnSig = await program.methods + .cnftFulfillBuy({ + root: getByteArray(nft.tree.root), + metadataHash: getByteArray(nft.tree.dataHash), + creatorHash: getByteArray(nft.tree.creatorHash), + nonce: new BN(nft.tree.nonce), + index: nft.nft.nftIndex, + buyerPrice: new BN(spotPrice * LAMPORTS_PER_SOL), + minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), + makerFeeBp: 0, + takerFeeBp: 100, + metadataArgs: { + name: metadataArgs.name, + symbol: metadataArgs.symbol, + uri: metadataArgs.uri, + sellerFeeBasisPoints: 0, // !!!! WRONG ROYALTY + primarySaleHappened: metadataArgs.primarySaleHappened, + isMutable: metadataArgs.isMutable, + editionNonce: isSome(metadataArgs.editionNonce) + ? metadataArgs.editionNonce.value + : null, + tokenStandard: isSome(metadataArgs.tokenStandard) + ? convertToDecodeTokenStandardEnum( + metadataArgs.tokenStandard.value, + ) + : null, + collection: isSome(metadataArgs.collection) + ? { + verified: metadataArgs.collection.value.verified, + key: new PublicKey(metadataArgs.collection.value.key), + } + : null, // Ensure it's a struct or null + uses: isSome(metadataArgs.uses) + ? { + useMethod: convertToDecodeUseMethodEnum( + metadataArgs.uses.value.useMethod, + ), + remaining: metadataArgs.uses.value.remaining, + total: metadataArgs.uses.value.total, + } + : null, + tokenProgramVersion: convertToDecodeTokenProgramVersion( + metadataArgs.tokenProgramVersion, + ), + creators: metadataArgs.creators.map((c) => ({ + address: new PublicKey(c.address), + verified: c.verified, + share: c.share, + })), + }, + }) + .accountsStrict({ + payer: new PublicKey(seller.publicKey), + owner: buyer.publicKey, + cosigner: cosigner.publicKey, + referral: poolData.referral.publicKey, + pool: poolData.poolKey, + buysideSolEscrowAccount, + treeAuthority, + merkleTree: nft.tree.merkleTree, + logWrapper: SPL_NOOP_PROGRAM_ID, + bubblegumProgram: MPL_BUBBLEGUM_PROGRAM_ID, + compressionProgram: SPL_ACCOUNT_COMPRESSION_PROGRAM_ID, + sellState, + systemProgram: SystemProgram.programId, + }) + .remainingAccounts([...creatorAccounts, ...proofPath]) + .signers([cosigner, seller.payer]) + // note: skipPreflight causes some weird error. + // so just surround in this try-catch to get the logs + .rpc(/* skipPreflight: true } */); + console.log(`fulfillBuyTxnSig: ${fulfillBuyTxnSig}`); + } catch (e) { + expect(e).toBeInstanceOf(SendTransactionError); + const err = e as SendTransactionError; + + assert.include( + err.message, + 'Invalid root recomputed from proof', + 'Error message should contain the expected substring', + ); + } + + console.log(`seller: ${seller.publicKey}`); + console.log(`buyer: ${buyer.publicKey}`); + console.log(`nft: ${JSON.stringify(nft)}`); + // Verify that seller still owns the cNFT. + await verifyOwnership( + umi, + merkleTree, + publicKey(seller.publicKey), + leafIndex, + metadata, + [], + ); + + // Get balances after fulfill buy + const [ + buyerAfter, + sellerAfter, + buyerSolEscrowAccountBalanceAfter, + creator1After, + creator2After, + ] = await Promise.all([ + connection.getBalance(buyer.publicKey), + connection.getBalance(seller.publicKey), + connection.getBalance(buysideSolEscrowAccount), + connection.getBalance(creatorAccounts[0].pubkey), + connection.getBalance(creatorAccounts[1].pubkey), + ]); + + console.log(`buyerAfter: ${buyerAfter}`); + console.log(`sellerAfter: ${sellerAfter}`); + console.log( + `buyerSolEscrowAccountBalanceAfter: ${buyerSolEscrowAccountBalanceAfter}`, + ); + console.log(`creator1After: ${creator1After}`); + console.log(`creator2After: ${creator2After}`); + + assert.equal(buyerBefore, buyerAfter); + + assert.equal( + buyerSolEscrowAccountBalanceBefore, + buyerSolEscrowAccountBalanceAfter, + ); + + // In production it should be seller buy tx fee, but with this test set up, buyer pays + // tx fee due to provider is initiated under buyer. + assert.equal(sellerAfter, sellerBefore); + + assert.equal(creator1After, creator1Before); + + assert.equal(creator2After, creator2Before); + }); }); From b08a8f117c7f694d84564d9719f83acf5eff47b6 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 13 Nov 2024 12:13:32 -0800 Subject: [PATCH 28/35] clean up tests log --- tests/mmm-cnft.spec.ts | 163 ----------------------------------------- tests/utils/cnft.ts | 24 ++---- 2 files changed, 6 insertions(+), 181 deletions(-) diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index e9b3d04..46dfc62 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -40,7 +40,6 @@ import { } from '@solana/web3.js'; import { findLeafAssetIdPda, - getAssetWithProof, getMetadataArgsSerializer, MetadataArgs, MPL_BUBBLEGUM_PROGRAM_ID, @@ -128,8 +127,6 @@ describe('cnft tests', () => { }); it('cnft fulfill buy - happy path', async () => { - console.log(`buyer: ${buyer.publicKey}`); - console.log(`seller: ${seller.publicKey}`); // 1. Create a tree. const { merkleTree, @@ -147,8 +144,6 @@ describe('cnft tests', () => { DEFAULT_TEST_SETUP_TREE_PARAMS, ); - const merkleyTreePubkey = getPubKey(merkleTree); - // 2. Create an offer. const { buysideSolEscrowAccount, poolData } = await createCNftCollectionOffer(program, { @@ -172,11 +167,6 @@ describe('cnft tests', () => { leafIndex, }); - // const asset = await umi.rpc.getAsset(assetId); - // console.log(`asset: ${JSON.stringify(asset)}`); - // const assetWithProof = await getAssetWithProof(umi, assetId); - // console.log(`assetWithProof: ${JSON.stringify(assetWithProof)}`); - const { key: sellState } = getMMMCnftSellStatePDA( program.programId, poolData.poolKey, @@ -199,18 +189,10 @@ describe('cnft tests', () => { nft.tree.merkleTree, ); - console.log(`merkleTree: ${nft.tree.merkleTree}`); - console.log(`proofs: ${nft.nft.fullProof}`); - console.log(`canopyDepth: ${treeAccount.getCanopyDepth()}`); - const proofPath: AccountMeta[] = getProofPath( nft.nft.fullProof, treeAccount.getCanopyDepth(), ); - console.log(`nft.nft.proofs.length: ${nft.nft.fullProof.length}`); - console.log(`proofPath.length: ${proofPath.length}`); - - console.log(`proofPath: ${JSON.stringify(proofPath)}`); const { accounts: creatorAccounts, @@ -218,7 +200,6 @@ describe('cnft tests', () => { creatorVerified, sellerFeeBasisPoints, } = getCreatorRoyaltiesArgs(creatorRoyalties); - console.log(`got creator royalties`); // get balances before fulfill buy const [ @@ -235,35 +216,12 @@ describe('cnft tests', () => { connection.getBalance(creatorAccounts[1].pubkey), ]); - console.log(`buyerBefore: ${buyerBefore}`); - console.log(`sellerBefore: ${sellerBefore}`); - console.log( - `buyerSolEscrowAccountBalanceBefore: ${buyerSolEscrowAccountBalanceBefore}`, - ); - console.log(`creator1Before: ${creator1Before}`); - console.log(`creator2Before: ${creator2Before}`); - try { const metadataSerializer = getMetadataArgsSerializer(); const metadataArgs: MetadataArgs = metadataSerializer.deserialize( metadataSerializer.serialize(metadata), )[0]; - console.log(`metadataArgs: ${JSON.stringify(metadataArgs)}`); - console.log( - `${JSON.stringify( - convertToDecodeTokenProgramVersion(metadataArgs.tokenProgramVersion), - )}`, - ); - - console.log(`expectedBuyPrices: { - sellerReceives: ${expectedBuyPrices.sellerReceives.toString(10)}, - lpFeePaid: ${expectedBuyPrices.lpFeePaid.toString(10)}, - royaltyPaid: ${expectedBuyPrices.royaltyPaid.toString(10)}, - takerFeePaid: ${expectedBuyPrices.takerFeePaid.toString(10)}, - makerFeePaid: ${expectedBuyPrices.makerFeePaid.toString(10)} - }`); - const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), @@ -335,7 +293,6 @@ describe('cnft tests', () => { // note: skipPreflight causes some weird error. // so just surround in this try-catch to get the logs .rpc(/* { skipPreflight: true } */); - console.log(`fulfillBuyTxnSig: ${fulfillBuyTxnSig}`); } catch (e) { if (e instanceof SendTransactionError) { const err = e as SendTransactionError; @@ -350,9 +307,6 @@ describe('cnft tests', () => { throw e; } - console.log(`seller: ${seller.publicKey}`); - console.log(`buyer: ${buyer.publicKey}`); - console.log(`nft: ${JSON.stringify(nft)}`); // Verify that buyer now owns the cNFT. await verifyOwnership( umi, @@ -378,14 +332,6 @@ describe('cnft tests', () => { connection.getBalance(creatorAccounts[1].pubkey), ]); - console.log(`buyerAfter: ${buyerAfter}`); - console.log(`sellerAfter: ${sellerAfter}`); - console.log( - `buyerSolEscrowAccountBalanceAfter: ${buyerSolEscrowAccountBalanceAfter}`, - ); - console.log(`creator1After: ${creator1After}`); - console.log(`creator2After: ${creator2After}`); - const expectedTxFees = SIGNATURE_FEE_LAMPORTS * 3; // cosigner + seller + payer (due to provider is under buyer) assert.equal(buyerBefore, buyerAfter + expectedTxFees); @@ -424,12 +370,7 @@ describe('cnft tests', () => { ); }); - // TODO: Add test for - // 1. Wrong metadata args (like collection) - // 2. trucate canopy it('cnft fulfill buy - incorrect collection fail allowlist check', async () => { - console.log(`buyer: ${buyer.publicKey}`); - console.log(`seller: ${seller.publicKey}`); // 1. Create a tree. const { merkleTree, @@ -447,8 +388,6 @@ describe('cnft tests', () => { DEFAULT_TEST_SETUP_TREE_PARAMS, ); - const merkleyTreePubkey = getPubKey(merkleTree); - // 2. Create an offer. const { buysideSolEscrowAccount, poolData } = await createCNftCollectionOffer(program, { @@ -472,11 +411,6 @@ describe('cnft tests', () => { leafIndex, }); - // const asset = await umi.rpc.getAsset(assetId); - // console.log(`asset: ${JSON.stringify(asset)}`); - // const assetWithProof = await getAssetWithProof(umi, assetId); - // console.log(`assetWithProof: ${JSON.stringify(assetWithProof)}`); - const { key: sellState } = getMMMCnftSellStatePDA( program.programId, poolData.poolKey, @@ -499,18 +433,10 @@ describe('cnft tests', () => { nft.tree.merkleTree, ); - console.log(`merkleTree: ${nft.tree.merkleTree}`); - console.log(`proofs: ${nft.nft.fullProof}`); - console.log(`canopyDepth: ${treeAccount.getCanopyDepth()}`); - const proofPath: AccountMeta[] = getProofPath( nft.nft.fullProof, treeAccount.getCanopyDepth(), ); - console.log(`nft.nft.proofs.length: ${nft.nft.fullProof.length}`); - console.log(`proofPath.length: ${proofPath.length}`); - - console.log(`proofPath: ${JSON.stringify(proofPath)}`); const { accounts: creatorAccounts, @@ -518,7 +444,6 @@ describe('cnft tests', () => { creatorVerified, sellerFeeBasisPoints, } = getCreatorRoyaltiesArgs(creatorRoyalties); - console.log(`got creator royalties`); // get balances before fulfill buy const [ @@ -535,35 +460,12 @@ describe('cnft tests', () => { connection.getBalance(creatorAccounts[1].pubkey), ]); - console.log(`buyerBefore: ${buyerBefore}`); - console.log(`sellerBefore: ${sellerBefore}`); - console.log( - `buyerSolEscrowAccountBalanceBefore: ${buyerSolEscrowAccountBalanceBefore}`, - ); - console.log(`creator1Before: ${creator1Before}`); - console.log(`creator2Before: ${creator2Before}`); - try { const metadataSerializer = getMetadataArgsSerializer(); const metadataArgs: MetadataArgs = metadataSerializer.deserialize( metadataSerializer.serialize(metadata), )[0]; - console.log(`metadataArgs: ${JSON.stringify(metadataArgs)}`); - console.log( - `${JSON.stringify( - convertToDecodeTokenProgramVersion(metadataArgs.tokenProgramVersion), - )}`, - ); - - console.log(`expectedBuyPrices: { - sellerReceives: ${expectedBuyPrices.sellerReceives.toString(10)}, - lpFeePaid: ${expectedBuyPrices.lpFeePaid.toString(10)}, - royaltyPaid: ${expectedBuyPrices.royaltyPaid.toString(10)}, - takerFeePaid: ${expectedBuyPrices.takerFeePaid.toString(10)}, - makerFeePaid: ${expectedBuyPrices.makerFeePaid.toString(10)} - }`); - const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), @@ -635,7 +537,6 @@ describe('cnft tests', () => { // note: skipPreflight causes some weird error. // so just surround in this try-catch to get the logs .rpc(/* { skipPreflight: true } */); - console.log(`fulfillBuyTxnSig: ${fulfillBuyTxnSig}`); } catch (e) { expect(e).toBeInstanceOf(anchor.AnchorError); const err = e as anchor.AnchorError; @@ -646,9 +547,6 @@ describe('cnft tests', () => { ); } - console.log(`seller: ${seller.publicKey}`); - console.log(`buyer: ${buyer.publicKey}`); - console.log(`nft: ${JSON.stringify(nft)}`); // Verify that seller still owns the cNFT. await verifyOwnership( umi, @@ -674,14 +572,6 @@ describe('cnft tests', () => { connection.getBalance(creatorAccounts[1].pubkey), ]); - console.log(`buyerAfter: ${buyerAfter}`); - console.log(`sellerAfter: ${sellerAfter}`); - console.log( - `buyerSolEscrowAccountBalanceAfter: ${buyerSolEscrowAccountBalanceAfter}`, - ); - console.log(`creator1After: ${creator1After}`); - console.log(`creator2After: ${creator2After}`); - assert.equal(buyerBefore, buyerAfter); assert.equal( @@ -699,8 +589,6 @@ describe('cnft tests', () => { }); it('cnft fulfill buy - incorrect royalty fail bubblegum check', async () => { - console.log(`buyer: ${buyer.publicKey}`); - console.log(`seller: ${seller.publicKey}`); // 1. Create a tree. const { merkleTree, @@ -718,8 +606,6 @@ describe('cnft tests', () => { DEFAULT_TEST_SETUP_TREE_PARAMS, ); - const merkleyTreePubkey = getPubKey(merkleTree); - // 2. Create an offer. const { buysideSolEscrowAccount, poolData } = await createCNftCollectionOffer(program, { @@ -743,11 +629,6 @@ describe('cnft tests', () => { leafIndex, }); - // const asset = await umi.rpc.getAsset(assetId); - // console.log(`asset: ${JSON.stringify(asset)}`); - // const assetWithProof = await getAssetWithProof(umi, assetId); - // console.log(`assetWithProof: ${JSON.stringify(assetWithProof)}`); - const { key: sellState } = getMMMCnftSellStatePDA( program.programId, poolData.poolKey, @@ -770,18 +651,10 @@ describe('cnft tests', () => { nft.tree.merkleTree, ); - console.log(`merkleTree: ${nft.tree.merkleTree}`); - console.log(`proofs: ${nft.nft.fullProof}`); - console.log(`canopyDepth: ${treeAccount.getCanopyDepth()}`); - const proofPath: AccountMeta[] = getProofPath( nft.nft.fullProof, treeAccount.getCanopyDepth(), ); - console.log(`nft.nft.proofs.length: ${nft.nft.fullProof.length}`); - console.log(`proofPath.length: ${proofPath.length}`); - - console.log(`proofPath: ${JSON.stringify(proofPath)}`); const { accounts: creatorAccounts, @@ -789,7 +662,6 @@ describe('cnft tests', () => { creatorVerified, sellerFeeBasisPoints, } = getCreatorRoyaltiesArgs(creatorRoyalties); - console.log(`got creator royalties`); // get balances before fulfill buy const [ @@ -806,35 +678,12 @@ describe('cnft tests', () => { connection.getBalance(creatorAccounts[1].pubkey), ]); - console.log(`buyerBefore: ${buyerBefore}`); - console.log(`sellerBefore: ${sellerBefore}`); - console.log( - `buyerSolEscrowAccountBalanceBefore: ${buyerSolEscrowAccountBalanceBefore}`, - ); - console.log(`creator1Before: ${creator1Before}`); - console.log(`creator2Before: ${creator2Before}`); - try { const metadataSerializer = getMetadataArgsSerializer(); const metadataArgs: MetadataArgs = metadataSerializer.deserialize( metadataSerializer.serialize(metadata), )[0]; - console.log(`metadataArgs: ${JSON.stringify(metadataArgs)}`); - console.log( - `${JSON.stringify( - convertToDecodeTokenProgramVersion(metadataArgs.tokenProgramVersion), - )}`, - ); - - console.log(`expectedBuyPrices: { - sellerReceives: ${expectedBuyPrices.sellerReceives.toString(10)}, - lpFeePaid: ${expectedBuyPrices.lpFeePaid.toString(10)}, - royaltyPaid: ${expectedBuyPrices.royaltyPaid.toString(10)}, - takerFeePaid: ${expectedBuyPrices.takerFeePaid.toString(10)}, - makerFeePaid: ${expectedBuyPrices.makerFeePaid.toString(10)} - }`); - const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), @@ -906,7 +755,6 @@ describe('cnft tests', () => { // note: skipPreflight causes some weird error. // so just surround in this try-catch to get the logs .rpc(/* skipPreflight: true } */); - console.log(`fulfillBuyTxnSig: ${fulfillBuyTxnSig}`); } catch (e) { expect(e).toBeInstanceOf(SendTransactionError); const err = e as SendTransactionError; @@ -918,9 +766,6 @@ describe('cnft tests', () => { ); } - console.log(`seller: ${seller.publicKey}`); - console.log(`buyer: ${buyer.publicKey}`); - console.log(`nft: ${JSON.stringify(nft)}`); // Verify that seller still owns the cNFT. await verifyOwnership( umi, @@ -946,14 +791,6 @@ describe('cnft tests', () => { connection.getBalance(creatorAccounts[1].pubkey), ]); - console.log(`buyerAfter: ${buyerAfter}`); - console.log(`sellerAfter: ${sellerAfter}`); - console.log( - `buyerSolEscrowAccountBalanceAfter: ${buyerSolEscrowAccountBalanceAfter}`, - ); - console.log(`creator1After: ${creator1After}`); - console.log(`creator2After: ${creator2After}`); - assert.equal(buyerBefore, buyerAfter); assert.equal( diff --git a/tests/utils/cnft.ts b/tests/utils/cnft.ts index 377de42..c91f785 100644 --- a/tests/utils/cnft.ts +++ b/tests/utils/cnft.ts @@ -18,7 +18,10 @@ import { MerkleTree, mintToCollectionV1, } from '@metaplex-foundation/mpl-bubblegum'; -import { createNft, mplTokenMetadata } from '@metaplex-foundation/mpl-token-metadata'; +import { + createNft, + mplTokenMetadata, +} from '@metaplex-foundation/mpl-token-metadata'; import { Context, generateSigner, @@ -232,11 +235,6 @@ export async function verifyOwnership( index: leafIndex, proof: currentProof, }).sendAndConfirm(umi); - console.log( - `verified ${expectedOwner} owns leaf at index ${leafIndex}. Result: ${JSON.stringify( - result, - )}`, - ); return { currentProof }; } @@ -265,14 +263,14 @@ export async function setupTree( const creatorSigners = await getCreatorPair(umi); const unverifiedCreators = await initUnverifiedCreatorsArray(creatorSigners); - const collectionMint = generateSigner(umi) + const collectionMint = generateSigner(umi); await createNft(umi, { mint: collectionMint, name: 'My Collection', uri: 'https://example.com/my-collection.json', sellerFeeBasisPoints: percentAmount(5), // % isCollection: true, - }).sendAndConfirm(umi); + }).sendAndConfirm(umi); const { metadata, leaf, leafIndex, creatorsHash, assetId } = await mint(umi, { merkleTree, @@ -284,8 +282,6 @@ export async function setupTree( }, }); - console.log(`metadata: ${JSON.stringify(metadata)}`); - const verifyCreatorProofTruncated = getTruncatedMerkleProof( treeParams.canopyDepth, [leaf], @@ -305,7 +301,6 @@ export async function setupTree( proof: verifyCreatorProofTruncated, }).sendAndConfirm(umi); - console.log(`verified creator A`); const updatedMetadata = { ...metadata, creators: [ @@ -358,13 +353,6 @@ export async function setupTree( [], ); - console.log(` - [setupTree] - fullProof(length: ${fullProof.length}): ${JSON.stringify(fullProof)} - sellerProof[truncated](length: ${sellerProof.length}): ${JSON.stringify( - sellerProof, - )} - `); return { merkleTree, leaf, From 1f7ca41ee01b15f4ff89a1da0d6f69e532c7e8ea Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 13 Nov 2024 14:54:11 -0800 Subject: [PATCH 29/35] add comment --- programs/mmm/src/instructions/cnft/metadata_args.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/metadata_args.rs b/programs/mmm/src/instructions/cnft/metadata_args.rs index 877c376..38ded03 100644 --- a/programs/mmm/src/instructions/cnft/metadata_args.rs +++ b/programs/mmm/src/instructions/cnft/metadata_args.rs @@ -1,6 +1,7 @@ use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; -// Define the TokenStandard enum +// Below types are copied from mpl bubblegum crate so +// IDL will automatically include them #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Hash)] pub enum TokenStandard { NonFungible, @@ -45,7 +46,6 @@ pub struct Creator { pub share: u8, } -// Define the MetadataArgs struct #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct MetadataArgs { pub name: String, From 42864c213e181a0603140a6da92fb07894f392bc Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 20 Nov 2024 11:34:13 -0800 Subject: [PATCH 30/35] clean up unused parameters --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 5 ---- programs/mmm/src/util.rs | 7 +---- sdk/src/idl/mmm.ts | 26 ------------------- tests/mmm-cnft.spec.ts | 6 ----- 4 files changed, 1 insertion(+), 43 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index db8614f..45265fa 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -24,9 +24,6 @@ pub struct SolCnftFulfillBuyArgs { // === cNFT transfer args === // // The Merkle root for the tree. Can be retrieved from off-chain data store. root: [u8; 32], - // The Keccak256 hash of the NFTs existing metadata (without the verified flag for the creator changed). - // The metadata is retrieved from off-chain data store. - metadata_hash: [u8; 32], // The Keccak256 hash of the NFTs existing creators array (without the verified flag for the creator changed). // The creators array is retrieved from off-chain data store. creator_hash: [u8; 32], @@ -38,8 +35,6 @@ pub struct SolCnftFulfillBuyArgs { index: u32, // === Contract args === // - // Price of the NFT in the payment_mint. - buyer_price: u64, pub min_payment_amount: u64, pub maker_fee_bp: i16, // will be checked by cosigner pub taker_fee_bp: i16, // will be checked by cosigner diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 8eeac3c..cad64b0 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -158,12 +158,7 @@ pub fn check_allowlists_for_mint( } pub fn check_allowlists_for_cnft(allowlists: &[Allowlist], collection: Collection) -> Result<()> { - // We need to check the following validation rules - // 1. make sure the metadata is correctly derived from the metadata pda with the mint - // 2. make sure mint+metadata(e.g. first verified creator address) can match one of the allowlist - // 3. note that the allowlist is unioned together, not intersection - // 4. skip if the allowlist.is_empty() - // 5. verify that nft either does not have master edition or is master edition + // Check mcc for cnft. for allowlist_val in allowlists.iter() { match allowlist_val.kind { ALLOWLIST_KIND_EMPTY => {} diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 33fa9dc..0c3d5a7 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2684,15 +2684,6 @@ export type Mmm = { ] } }, - { - "name": "metadataHash", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "creatorHash", "type": { @@ -2710,10 +2701,6 @@ export type Mmm = { "name": "index", "type": "u32" }, - { - "name": "buyerPrice", - "type": "u64" - }, { "name": "minPaymentAmount", "type": "u64" @@ -5969,15 +5956,6 @@ export const IDL: Mmm = { ] } }, - { - "name": "metadataHash", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "creatorHash", "type": { @@ -5995,10 +5973,6 @@ export const IDL: Mmm = { "name": "index", "type": "u32" }, - { - "name": "buyerPrice", - "type": "u64" - }, { "name": "minPaymentAmount", "type": "u64" diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 46dfc62..86bf03b 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -225,11 +225,9 @@ describe('cnft tests', () => { const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), - metadataHash: getByteArray(nft.tree.dataHash), creatorHash: getByteArray(nft.tree.creatorHash), nonce: new BN(nft.tree.nonce), index: nft.nft.nftIndex, - buyerPrice: new BN(spotPrice * LAMPORTS_PER_SOL), minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), makerFeeBp: 0, takerFeeBp: 100, @@ -469,11 +467,9 @@ describe('cnft tests', () => { const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), - metadataHash: getByteArray(nft.tree.dataHash), creatorHash: getByteArray(nft.tree.creatorHash), nonce: new BN(nft.tree.nonce), index: nft.nft.nftIndex, - buyerPrice: new BN(spotPrice * LAMPORTS_PER_SOL), minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), makerFeeBp: 0, takerFeeBp: 100, @@ -687,11 +683,9 @@ describe('cnft tests', () => { const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ root: getByteArray(nft.tree.root), - metadataHash: getByteArray(nft.tree.dataHash), creatorHash: getByteArray(nft.tree.creatorHash), nonce: new BN(nft.tree.nonce), index: nft.nft.nftIndex, - buyerPrice: new BN(spotPrice * LAMPORTS_PER_SOL), minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), makerFeeBp: 0, takerFeeBp: 100, From 573d61bfe0d36b32dfe3eaaa5b9b071171a99acd Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 20 Nov 2024 14:07:57 -0800 Subject: [PATCH 31/35] use asset id as seed of sell state --- programs/mmm/src/errors.rs | 2 ++ .../instructions/cnft/sol_cnft_fulfill_buy.rs | 11 ++++++---- programs/mmm/src/util.rs | 1 + sdk/src/cnft.ts | 18 ----------------- sdk/src/idl/mmm.ts | 18 +++++++++++++++++ tests/mmm-cnft.spec.ts | 20 +++++++++---------- 6 files changed, 38 insertions(+), 32 deletions(-) diff --git a/programs/mmm/src/errors.rs b/programs/mmm/src/errors.rs index f907950..5fbb718 100644 --- a/programs/mmm/src/errors.rs +++ b/programs/mmm/src/errors.rs @@ -78,4 +78,6 @@ pub enum MMMErrorCode { InvalidCnftCreators, // 0x1794 #[msg("Invalid cnft metadata")] InvalidCnftMetadata, // 0x1795 + #[msg("Invalid cnft metadata args")] + InvalidCnftMetadataArgs, // 0x1796 } diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index 45265fa..b18865a 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -6,7 +6,7 @@ use crate::{ constants::*, errors::MMMErrorCode, index_ra, - state::{BubblegumProgram, Pool, SellState, TreeConfigAnchor}, + state::{BubblegumProgram, Pool, SellState}, util::{ assert_valid_fees_bp, check_allowlists_for_cnft, check_remaining_accounts_for_m2, get_buyside_seller_receives, get_lp_fee_bp, get_sol_fee, get_sol_lp_fee, @@ -22,6 +22,7 @@ use super::MetadataArgs; #[derive(AnchorSerialize, AnchorDeserialize)] pub struct SolCnftFulfillBuyArgs { // === cNFT transfer args === // + asset_id: Pubkey, // The Merkle root for the tree. Can be retrieved from off-chain data store. root: [u8; 32], // The Keccak256 hash of the NFTs existing creators array (without the verified flag for the creator changed). @@ -86,7 +87,7 @@ pub struct SolCnftFulfillBuy<'info> { bump, )] /// CHECK: This account is neither written to nor read from. - pub tree_authority: Account<'info, TreeConfigAnchor>, + pub tree_authority: UncheckedAccount<'info>, // The account that contains the Merkle tree, initialized by create_tree. /// CHECK: This account is modified in the downstream Bubblegum program @@ -109,8 +110,7 @@ pub struct SolCnftFulfillBuy<'info> { seeds = [ SELL_STATE_PREFIX.as_bytes(), pool.key().as_ref(), - merkle_tree.key().as_ref(), - args.index.to_le_bytes().as_ref(), + args.asset_id.as_ref(), ], space = SellState::LEN, bump @@ -231,6 +231,9 @@ pub fn handler<'info>( // 3. Transfer CNFT to buyer (pool or owner) let data_hash = hash_metadata(&args.metadata_args)?; let asset_mint = get_asset_id(&merkle_tree.key(), args.nonce); + if asset_mint != args.asset_id { + return Err(MMMErrorCode::InvalidCnftMetadataArgs.into()); + } // reinvest fulfill buy is just a placeholder for now if we want to enable double sided // pool for cnft in the the future. if pool.reinvest_fulfill_buy { diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index cad64b0..fd34695 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -1226,6 +1226,7 @@ pub fn create_core_metadata_core(royalties: &Royalties) -> MplCoreMetadata { } } +// TODO: use the transfer cpi builder by mpl bubblegum #[allow(clippy::too_many_arguments)] pub fn transfer_compressed_nft<'info>( tree_authority: &AccountInfo<'info>, diff --git a/sdk/src/cnft.ts b/sdk/src/cnft.ts index b93c64c..5260bce 100644 --- a/sdk/src/cnft.ts +++ b/sdk/src/cnft.ts @@ -52,24 +52,6 @@ export function getByteArray(key: PublicKey): Array { return Array.from(key.toBuffer()); } -export const getMMMCnftSellStatePDA = ( - programId: PublicKey, - pool: PublicKey, - merkleTree: PublicKey, - index: number, -) => { - const [key, bump] = PublicKey.findProgramAddressSync( - [ - Buffer.from(PREFIXES.SELL_STATE), - pool.toBuffer(), - merkleTree.toBuffer(), - new BN(index).toBuffer('le', 4), - ], - programId, - ); - return { key, bump }; -}; - // get "proof path" from asset proof, these are the accounts that need to be passed to the program as remaining accounts // may also be empty if tree is small enough, and canopy depth is large enough export function getProofPath( diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 0c3d5a7..a544fcb 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2675,6 +2675,10 @@ export type Mmm = { "type": { "kind": "struct", "fields": [ + { + "name": "assetId", + "type": "publicKey" + }, { "name": "root", "type": { @@ -3266,6 +3270,11 @@ export type Mmm = { "code": 6037, "name": "InvalidCnftMetadata", "msg": "Invalid cnft metadata" + }, + { + "code": 6038, + "name": "InvalidCnftMetadataArgs", + "msg": "Invalid cnft metadata args" } ] }; @@ -5947,6 +5956,10 @@ export const IDL: Mmm = { "type": { "kind": "struct", "fields": [ + { + "name": "assetId", + "type": "publicKey" + }, { "name": "root", "type": { @@ -6538,6 +6551,11 @@ export const IDL: Mmm = { "code": 6037, "name": "InvalidCnftMetadata", "msg": "Invalid cnft metadata" + }, + { + "code": 6038, + "name": "InvalidCnftMetadataArgs", + "msg": "Invalid cnft metadata args" } ] }; diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 86bf03b..84c64f9 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -23,7 +23,7 @@ import { getByteArray, getM2BuyerSharedEscrow, getMMMBuysideSolEscrowPDA, - getMMMCnftSellStatePDA, + getMMMSellStatePDA, getProofPath, getSolFulfillBuyPrices, IDL, @@ -167,11 +167,10 @@ describe('cnft tests', () => { leafIndex, }); - const { key: sellState } = getMMMCnftSellStatePDA( + const { key: sellState } = getMMMSellStatePDA( program.programId, poolData.poolKey, - new PublicKey(nft.tree.merkleTree), - nft.nft.nftIndex, + new PublicKey(assetId), ); const spotPrice = 1; @@ -224,6 +223,7 @@ describe('cnft tests', () => { const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ + assetId: new PublicKey(assetId), root: getByteArray(nft.tree.root), creatorHash: getByteArray(nft.tree.creatorHash), nonce: new BN(nft.tree.nonce), @@ -409,11 +409,10 @@ describe('cnft tests', () => { leafIndex, }); - const { key: sellState } = getMMMCnftSellStatePDA( + const { key: sellState } = getMMMSellStatePDA( program.programId, poolData.poolKey, - new PublicKey(nft.tree.merkleTree), - nft.nft.nftIndex, + new PublicKey(assetId), ); const spotPrice = 1; @@ -466,6 +465,7 @@ describe('cnft tests', () => { const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ + assetId: new PublicKey(assetId), root: getByteArray(nft.tree.root), creatorHash: getByteArray(nft.tree.creatorHash), nonce: new BN(nft.tree.nonce), @@ -625,11 +625,10 @@ describe('cnft tests', () => { leafIndex, }); - const { key: sellState } = getMMMCnftSellStatePDA( + const { key: sellState } = getMMMSellStatePDA( program.programId, poolData.poolKey, - new PublicKey(nft.tree.merkleTree), - nft.nft.nftIndex, + new PublicKey(assetId), ); const spotPrice = 1; @@ -682,6 +681,7 @@ describe('cnft tests', () => { const fulfillBuyTxnSig = await program.methods .cnftFulfillBuy({ + assetId: new PublicKey(assetId), root: getByteArray(nft.tree.root), creatorHash: getByteArray(nft.tree.creatorHash), nonce: new BN(nft.tree.nonce), From 609e72e1abdfc35c75e23afdf2c6239d267263b9 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 20 Nov 2024 14:30:08 -0800 Subject: [PATCH 32/35] compute creator hash using metadata args --- .../instructions/cnft/sol_cnft_fulfill_buy.rs | 35 ++++--------------- programs/mmm/src/util.rs | 31 ++++++++-------- sdk/src/idl/mmm.ts | 18 ---------- tests/mmm-cnft.spec.ts | 3 -- 4 files changed, 22 insertions(+), 65 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs index b18865a..70b09b3 100644 --- a/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs +++ b/programs/mmm/src/instructions/cnft/sol_cnft_fulfill_buy.rs @@ -10,9 +10,9 @@ use crate::{ util::{ assert_valid_fees_bp, check_allowlists_for_cnft, check_remaining_accounts_for_m2, get_buyside_seller_receives, get_lp_fee_bp, get_sol_fee, get_sol_lp_fee, - get_sol_total_price_and_next_price, hash_metadata, log_pool, pay_creator_fees_in_sol_cnft, - transfer_compressed_nft, try_close_escrow, try_close_pool, try_close_sell_state, - verify_creators, withdraw_m2, + get_sol_total_price_and_next_price, hash_creators_from_metadata_args, hash_metadata, + log_pool, pay_creator_fees_in_sol_cnft, transfer_compressed_nft, try_close_escrow, + try_close_pool, try_close_sell_state, withdraw_m2, }, verify_referral::verify_referral, }; @@ -25,9 +25,6 @@ pub struct SolCnftFulfillBuyArgs { asset_id: Pubkey, // The Merkle root for the tree. Can be retrieved from off-chain data store. root: [u8; 32], - // The Keccak256 hash of the NFTs existing creators array (without the verified flag for the creator changed). - // The creators array is retrieved from off-chain data store. - creator_hash: [u8; 32], // A nonce ("number used once") value used to make the Merkle tree leaves unique. // This is the value of num_minted for the tree stored in the TreeConfig account at the time the NFT was minted. // The unique value for each asset can be retrieved from off-chain data store. @@ -207,26 +204,8 @@ pub fn handler<'info>( remaining_accounts.split_at(creator_length) }; - let creator_shares = args - .metadata_args - .creators - .iter() - .map(|c| c.share as u16) - .collect::>(); - - let creator_verified = args - .metadata_args - .creators - .iter() - .map(|c| c.verified) - .collect(); - - verify_creators( - creator_accounts.iter(), - creator_shares, - creator_verified, - args.creator_hash, - )?; + let creator_hash = + hash_creators_from_metadata_args(creator_accounts.iter(), &args.metadata_args)?; // 3. Transfer CNFT to buyer (pool or owner) let data_hash = hash_metadata(&args.metadata_args)?; @@ -253,7 +232,7 @@ pub fn handler<'info>( ctx.accounts.bubblegum_program.key(), args.root, data_hash, - args.creator_hash, + creator_hash, args.nonce, args.index, None, // signer passed through from ctx @@ -284,7 +263,7 @@ pub fn handler<'info>( ctx.accounts.bubblegum_program.key(), args.root, data_hash, - args.creator_hash, + creator_hash, args.nonce, args.index, None, // signer passed through from ctx diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index fd34695..0d99503 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -1331,12 +1331,21 @@ pub fn hash_metadata(metadata: &MetadataArgs) -> Result<[u8; 32]> { .to_bytes()) } -pub fn verify_creators( +pub fn hash_creators_from_metadata_args( creator_accounts: Iter, - creator_shares: Vec, - creator_verified: Vec, - creator_hash: [u8; 32], -) -> Result<()> { + metadata_args: &MetadataArgs, +) -> Result<[u8; 32]> { + let creator_shares = metadata_args + .creators + .iter() + .map(|c| c.share as u16) + .collect::>(); + + let creator_verified = metadata_args + .creators + .iter() + .map(|c| c.verified) + .collect::>(); // Check that all input arrays/vectors are of the same length if creator_accounts.len() != creator_shares.len() || creator_accounts.len() != creator_verified.len() @@ -1360,17 +1369,7 @@ pub fn verify_creators( // Compute the hash from the Creator vector let computed_hash = hash_creators(&creators); - // Compare the computed hash with the provided hash - if computed_hash != creator_hash { - msg!( - "Computed hash does not match provided hash: {{\"computed\":{:?},\"provided\":{:?}}}", - computed_hash, - creator_hash - ); - return Err(MMMErrorCode::InvalidCnftCreators.into()); - } - - Ok(()) + Ok(computed_hash) } #[cfg(test)] diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index a544fcb..5483698 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -2688,15 +2688,6 @@ export type Mmm = { ] } }, - { - "name": "creatorHash", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "nonce", "type": "u64" @@ -5969,15 +5960,6 @@ export const IDL: Mmm = { ] } }, - { - "name": "creatorHash", - "type": { - "array": [ - "u8", - 32 - ] - } - }, { "name": "nonce", "type": "u64" diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index 84c64f9..afa16f9 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -225,7 +225,6 @@ describe('cnft tests', () => { .cnftFulfillBuy({ assetId: new PublicKey(assetId), root: getByteArray(nft.tree.root), - creatorHash: getByteArray(nft.tree.creatorHash), nonce: new BN(nft.tree.nonce), index: nft.nft.nftIndex, minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), @@ -467,7 +466,6 @@ describe('cnft tests', () => { .cnftFulfillBuy({ assetId: new PublicKey(assetId), root: getByteArray(nft.tree.root), - creatorHash: getByteArray(nft.tree.creatorHash), nonce: new BN(nft.tree.nonce), index: nft.nft.nftIndex, minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), @@ -683,7 +681,6 @@ describe('cnft tests', () => { .cnftFulfillBuy({ assetId: new PublicKey(assetId), root: getByteArray(nft.tree.root), - creatorHash: getByteArray(nft.tree.creatorHash), nonce: new BN(nft.tree.nonce), index: nft.nft.nftIndex, minPaymentAmount: new BN(expectedBuyPrices.sellerReceives), From 7112056d50a5c12b1687db18d98b7ec0dd9aa193 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Wed, 20 Nov 2024 23:30:32 -0800 Subject: [PATCH 33/35] address comments --- programs/mmm/src/errors.rs | 4 ---- programs/mmm/src/util.rs | 2 +- sdk/src/idl/mmm.ts | 24 ++---------------------- tests/mmm-cnft.spec.ts | 6 ------ tests/utils/cnft.ts | 5 ----- 5 files changed, 3 insertions(+), 38 deletions(-) diff --git a/programs/mmm/src/errors.rs b/programs/mmm/src/errors.rs index 5fbb718..5787a9f 100644 --- a/programs/mmm/src/errors.rs +++ b/programs/mmm/src/errors.rs @@ -72,10 +72,6 @@ pub enum MMMErrorCode { InvalidTokenExtension, // 0x1791 #[msg("Unsupported asset plugin")] UnsupportedAssetPlugin, // 0x1792 - #[msg("Mismatched creator data lengths")] - MismatchedCreatorDataLengths, // 0x1793 - #[msg("Invalid cnft creators")] - InvalidCnftCreators, // 0x1794 #[msg("Invalid cnft metadata")] InvalidCnftMetadata, // 0x1795 #[msg("Invalid cnft metadata args")] diff --git a/programs/mmm/src/util.rs b/programs/mmm/src/util.rs index 0d99503..ce950a1 100644 --- a/programs/mmm/src/util.rs +++ b/programs/mmm/src/util.rs @@ -1350,7 +1350,7 @@ pub fn hash_creators_from_metadata_args( if creator_accounts.len() != creator_shares.len() || creator_accounts.len() != creator_verified.len() { - return Err(MMMErrorCode::MismatchedCreatorDataLengths.into()); + return Err(MMMErrorCode::InvalidCreatorAddress.into()); } // Convert input data to a vector of Creator structs diff --git a/sdk/src/idl/mmm.ts b/sdk/src/idl/mmm.ts index 5483698..59e1407 100644 --- a/sdk/src/idl/mmm.ts +++ b/sdk/src/idl/mmm.ts @@ -3249,21 +3249,11 @@ export type Mmm = { }, { "code": 6035, - "name": "MismatchedCreatorDataLengths", - "msg": "Mismatched creator data lengths" - }, - { - "code": 6036, - "name": "InvalidCnftCreators", - "msg": "Invalid cnft creators" - }, - { - "code": 6037, "name": "InvalidCnftMetadata", "msg": "Invalid cnft metadata" }, { - "code": 6038, + "code": 6036, "name": "InvalidCnftMetadataArgs", "msg": "Invalid cnft metadata args" } @@ -6521,21 +6511,11 @@ export const IDL: Mmm = { }, { "code": 6035, - "name": "MismatchedCreatorDataLengths", - "msg": "Mismatched creator data lengths" - }, - { - "code": 6036, - "name": "InvalidCnftCreators", - "msg": "Invalid cnft creators" - }, - { - "code": 6037, "name": "InvalidCnftMetadata", "msg": "Invalid cnft metadata" }, { - "code": 6038, + "code": 6036, "name": "InvalidCnftMetadataArgs", "msg": "Invalid cnft metadata args" } diff --git a/tests/mmm-cnft.spec.ts b/tests/mmm-cnft.spec.ts index afa16f9..80ff88e 100644 --- a/tests/mmm-cnft.spec.ts +++ b/tests/mmm-cnft.spec.ts @@ -338,8 +338,6 @@ describe('cnft tests', () => { buyerSolEscrowAccountBalanceAfter + spotPrice * LAMPORTS_PER_SOL, ); - // In production it should be seller buy tx fee, but with this test set up, buyer pays - // tx fee due to provider is initiated under buyer. assert.equal( sellerAfter, sellerBefore + @@ -573,8 +571,6 @@ describe('cnft tests', () => { buyerSolEscrowAccountBalanceAfter, ); - // In production it should be seller buy tx fee, but with this test set up, buyer pays - // tx fee due to provider is initiated under buyer. assert.equal(sellerAfter, sellerBefore); assert.equal(creator1After, creator1Before); @@ -789,8 +785,6 @@ describe('cnft tests', () => { buyerSolEscrowAccountBalanceAfter, ); - // In production it should be seller buy tx fee, but with this test set up, buyer pays - // tx fee due to provider is initiated under buyer. assert.equal(sellerAfter, sellerBefore); assert.equal(creator1After, creator1Before); diff --git a/tests/utils/cnft.ts b/tests/utils/cnft.ts index c91f785..4e0e629 100644 --- a/tests/utils/cnft.ts +++ b/tests/utils/cnft.ts @@ -51,8 +51,6 @@ export const ME_TREASURY = new Web3PubKey( 'rFqFJ9g7TGBD8Ed7TPDnvGKZ5pWLPDyxLcvcH2eRCtt', ); -export const treasury = publicKey(ME_TREASURY.toBase58()); - export const createUmi = async (endpoint?: string, airdropAmount?: SolAmount) => (await baseCreateUmi(endpoint, { commitment: 'confirmed' }, airdropAmount)) .use(mplTokenMetadata()) @@ -176,9 +174,6 @@ export const mint = async ( }; }; -// This is Hash(metadataArgs). Useful for verifying sellers fee basis points are valid. -// NOTE: this does not perform any checks on the hash, it is recommended to use getMetadataHashChecked -// in production!! export function hashMetadataArgsArgs(metadata: MetadataArgsArgs): Uint8Array { return hash(getMetadataArgsSerializer().serialize(metadata)); } From a1ede67def5086fff4ef6f814a0429c468f37fe6 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Fri, 22 Nov 2024 13:55:32 -0800 Subject: [PATCH 34/35] skip ocp test --- tests/mmm-ocp.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/mmm-ocp.spec.ts b/tests/mmm-ocp.spec.ts index c9b2714..7d57480 100644 --- a/tests/mmm-ocp.spec.ts +++ b/tests/mmm-ocp.spec.ts @@ -42,7 +42,7 @@ import { PROGRAM_ID as OCP_PROGRAM_ID, } from '@magiceden-oss/open_creator_protocol'; -describe('mmm-ocp', () => { +describe.skip('mmm-ocp', () => { const { connection } = anchor.AnchorProvider.env(); const wallet = new anchor.Wallet(Keypair.generate()); const provider = new anchor.AnchorProvider(connection, wallet, { From 6cbe701b048ec9c27b8b5bc75aa55cf97284ecf9 Mon Sep 17 00:00:00 2001 From: JeremyLi28 Date: Fri, 22 Nov 2024 14:12:48 -0800 Subject: [PATCH 35/35] fix format --- programs/mmm/src/instructions/cnft/mod.rs | 4 ++-- programs/mmm/src/instructions/mod.rs | 4 ++-- programs/mmm/src/state.rs | 2 -- sdk/src/mmmClient.ts | 3 ++- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/programs/mmm/src/instructions/cnft/mod.rs b/programs/mmm/src/instructions/cnft/mod.rs index 3dd87ea..ba9e952 100644 --- a/programs/mmm/src/instructions/cnft/mod.rs +++ b/programs/mmm/src/instructions/cnft/mod.rs @@ -1,5 +1,5 @@ -pub mod sol_cnft_fulfill_buy; pub mod metadata_args; +pub mod sol_cnft_fulfill_buy; +pub use metadata_args::*; pub use sol_cnft_fulfill_buy::*; -pub use metadata_args::*; \ No newline at end of file diff --git a/programs/mmm/src/instructions/mod.rs b/programs/mmm/src/instructions/mod.rs index f6157d7..753895e 100644 --- a/programs/mmm/src/instructions/mod.rs +++ b/programs/mmm/src/instructions/mod.rs @@ -1,20 +1,20 @@ #![allow(missing_docs)] pub mod admin; +pub mod cnft; pub mod ext_vanilla; pub mod mip1; pub mod mpl_core_asset; pub mod ocp; pub mod vanilla; -pub mod cnft; pub use admin::*; +pub use cnft::*; pub use ext_vanilla::*; pub use mip1::*; pub use mpl_core_asset::*; pub use ocp::*; pub use vanilla::*; -pub use cnft::*; use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; diff --git a/programs/mmm/src/state.rs b/programs/mmm/src/state.rs index 1be05ee..fbb248d 100644 --- a/programs/mmm/src/state.rs +++ b/programs/mmm/src/state.rs @@ -138,7 +138,6 @@ impl SellState { 200; // padding } - // Wrapper structs to replace the Anchor program types until the Metaplex libs have // better Anchor support. pub struct BubblegumProgram; @@ -149,7 +148,6 @@ impl Id for BubblegumProgram { } } - #[derive(Clone)] pub struct TreeConfigAnchor(pub TreeConfig); diff --git a/sdk/src/mmmClient.ts b/sdk/src/mmmClient.ts index 83020a0..fee18d8 100644 --- a/sdk/src/mmmClient.ts +++ b/sdk/src/mmmClient.ts @@ -305,7 +305,8 @@ export class MMMClient { | ReturnType | ReturnType | ReturnType - | ReturnType; + | ReturnType + | ReturnType; const mintOrCoreAsset = await this.conn.getAccountInfo(assetMint); let { key: buysideSolEscrowAccount } = getMMMBuysideSolEscrowPDA(