diff --git a/examples/next-js/package-lock.json b/examples/next-js/package-lock.json index cbc189e..169bb15 100644 --- a/examples/next-js/package-lock.json +++ b/examples/next-js/package-lock.json @@ -9,6 +9,8 @@ "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^3.6.0", + "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", + "@metaplex-foundation/umi-web3js-adapters": "^0.9.2", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -16,12 +18,14 @@ "@radix-ui/react-slot": "^1.0.2", "@solana/actions": "^1.6.4", "@solana/qr-code-styling": "^1.6.0", + "@solana/spl-token": "^0.4.9", "@solana/web3.js": "^1.95.3", "bs58": "^5.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cross-fetch": "^4.0.0", "js-base64": "^3.7.7", + "lighthouse-sdk-legacy": "^2.0.0", "lucide-react": "^0.390.0", "next": "14.2.3", "next-themes": "^0.3.0", @@ -289,6 +293,196 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@metaplex-foundation/umi": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi/-/umi-0.9.2.tgz", + "integrity": "sha512-9i4Acm4pruQfJcpRrc2EauPBwkfDN0I9QTvJyZocIlKgoZwD6A6wH0PViH1AjOVG5CQCd1YI3tJd5XjYE1ElBw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-options": "^0.8.9", + "@metaplex-foundation/umi-public-keys": "^0.8.9", + "@metaplex-foundation/umi-serializers": "^0.9.0" + } + }, + "node_modules/@metaplex-foundation/umi-bundle-defaults": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-bundle-defaults/-/umi-bundle-defaults-0.9.2.tgz", + "integrity": "sha512-kV3tfvgvRjVP1p9OFOtH+ibOtN9omVJSwKr0We4/9r45e5LTj+32su0V/rixZUkG1EZzzOYBsxhtIE0kIw/Hrw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-downloader-http": "^0.9.2", + "@metaplex-foundation/umi-eddsa-web3js": "^0.9.2", + "@metaplex-foundation/umi-http-fetch": "^0.9.2", + "@metaplex-foundation/umi-program-repository": "^0.9.2", + "@metaplex-foundation/umi-rpc-chunk-get-accounts": "^0.9.2", + "@metaplex-foundation/umi-rpc-web3js": "^0.9.2", + "@metaplex-foundation/umi-serializer-data-view": "^0.9.2", + "@metaplex-foundation/umi-transaction-factory-web3js": "^0.9.2" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-downloader-http": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-downloader-http/-/umi-downloader-http-0.9.2.tgz", + "integrity": "sha512-tzPT9hBwenzTzAQg07rmsrqZfgguAXELbcJrsYMoASp5VqWFXYIP00g94KET6XLjWUXH4P1J2zoa6hGennPXHA==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2" + } + }, + "node_modules/@metaplex-foundation/umi-eddsa-web3js": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-eddsa-web3js/-/umi-eddsa-web3js-0.9.2.tgz", + "integrity": "sha512-hhPCxXbYIp4BC4z9gK78sXpWLkNSrfv4ndhF5ruAkdIp7GcRVYKj0QnOUO6lGYGiIkNlw20yoTwOe1CT//OfTQ==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-web3js-adapters": "^0.9.2", + "@noble/curves": "^1.0.0" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-http-fetch": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-http-fetch/-/umi-http-fetch-0.9.2.tgz", + "integrity": "sha512-YCZuBu24T9ZzEDe4+w12LEZm/fO9pkyViZufGgASC5NX93814Lvf6Ssjn/hZzjfA7CvZbvLFbmujc6CV3Q/m9Q==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.7" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2" + } + }, + "node_modules/@metaplex-foundation/umi-options": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-options/-/umi-options-0.8.9.tgz", + "integrity": "sha512-jSQ61sZMPSAk/TXn8v8fPqtz3x8d0/blVZXLLbpVbo2/T5XobiI6/MfmlUosAjAUaQl6bHRF8aIIqZEFkJiy4A==", + "license": "MIT" + }, + "node_modules/@metaplex-foundation/umi-program-repository": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-program-repository/-/umi-program-repository-0.9.2.tgz", + "integrity": "sha512-g3+FPqXEmYsBa8eETtUE2gb2Oe3mqac0z3/Ur1TvAg5TtIy3mzRzOy/nza+sgzejnfcxcVg835rmpBaxpBnjDA==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2" + } + }, + "node_modules/@metaplex-foundation/umi-public-keys": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-public-keys/-/umi-public-keys-0.8.9.tgz", + "integrity": "sha512-CxMzN7dgVGOq9OcNCJe2casKUpJ3RmTVoOvDFyeoTQuK+vkZ1YSSahbqC1iGuHEtKTLSjtWjKvUU6O7zWFTw3Q==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-serializers-encodings": "^0.8.9" + } + }, + "node_modules/@metaplex-foundation/umi-rpc-chunk-get-accounts": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-rpc-chunk-get-accounts/-/umi-rpc-chunk-get-accounts-0.9.2.tgz", + "integrity": "sha512-YRwVf6xH0jPBAUgMhEPi+UbjioAeqTXmjsN2TnmQCPAmHbrHrMRj0rlWYwFLWAgkmoxazYrXP9lqOFRrfOGAEA==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2" + } + }, + "node_modules/@metaplex-foundation/umi-rpc-web3js": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-rpc-web3js/-/umi-rpc-web3js-0.9.2.tgz", + "integrity": "sha512-MqcsBz8B4wGl6jxsf2Jo/rAEpYReU9VCSR15QSjhvADHMmdFxCIZCCAgE+gDE2Vuanfl437VhOcP3g5Uw8C16Q==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-web3js-adapters": "^0.9.2" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-serializer-data-view": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializer-data-view/-/umi-serializer-data-view-0.9.2.tgz", + "integrity": "sha512-5vGptadJxUxvUcyrwFZxXlEc6Q7AYySBesizCtrBFUY8w8PnF2vzmS45CP1MLySEATNH6T9mD4Rs0tLb87iQyA==", + "license": "MIT", + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2" + } + }, + "node_modules/@metaplex-foundation/umi-serializers": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers/-/umi-serializers-0.9.0.tgz", + "integrity": "sha512-hAOW9Djl4w4ioKeR4erDZl5IG4iJdP0xA19ZomdaCbMhYAAmG/FEs5khh0uT2mq53/MnzWcXSUPoO8WBN4Q+Vg==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-options": "^0.8.9", + "@metaplex-foundation/umi-public-keys": "^0.8.9", + "@metaplex-foundation/umi-serializers-core": "^0.8.9", + "@metaplex-foundation/umi-serializers-encodings": "^0.8.9", + "@metaplex-foundation/umi-serializers-numbers": "^0.8.9" + } + }, + "node_modules/@metaplex-foundation/umi-serializers-core": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-core/-/umi-serializers-core-0.8.9.tgz", + "integrity": "sha512-WT82tkiYJ0Qmscp7uTj1Hz6aWQPETwaKLAENAUN5DeWghkuBKtuxyBKVvEOuoXerJSdhiAk0e8DWA4cxcTTQ/w==", + "license": "MIT" + }, + "node_modules/@metaplex-foundation/umi-serializers-encodings": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-encodings/-/umi-serializers-encodings-0.8.9.tgz", + "integrity": "sha512-N3VWLDTJ0bzzMKcJDL08U3FaqRmwlN79FyE4BHj6bbAaJ9LEHjDQ9RJijZyWqTm0jE7I750fU7Ow5EZL38Xi6Q==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-serializers-core": "^0.8.9" + } + }, + "node_modules/@metaplex-foundation/umi-serializers-numbers": { + "version": "0.8.9", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-serializers-numbers/-/umi-serializers-numbers-0.8.9.tgz", + "integrity": "sha512-NtBf1fnVNQJHFQjLFzRu2i9GGnigb9hOm/Gfrk628d0q0tRJB7BOM3bs5C61VAs7kJs4yd+pDNVAERJkknQ7Lg==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-serializers-core": "^0.8.9" + } + }, + "node_modules/@metaplex-foundation/umi-transaction-factory-web3js": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-transaction-factory-web3js/-/umi-transaction-factory-web3js-0.9.2.tgz", + "integrity": "sha512-fR1Kf21uylMFd1Smkltmj4jTNxhqSWf416owsJ+T+cvJi2VCOcOwq/3UFzOrpz78fA0RhsajKYKj0HYsRnQI1g==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi-web3js-adapters": "^0.9.2" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@metaplex-foundation/umi-web3js-adapters": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@metaplex-foundation/umi-web3js-adapters/-/umi-web3js-adapters-0.9.2.tgz", + "integrity": "sha512-RQqUTtHYY9fmEMnq7s3Hiv/81flGaoI0ZVVoafnFVaQLnxU6QBKxtboRZHk43XtD9CiFh5f9izrMJX7iK7KlOA==", + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3" + }, + "peerDependencies": { + "@metaplex-foundation/umi": "^0.9.2", + "@solana/web3.js": "^1.72.0" + } + }, + "node_modules/@minhducsun2002/leb128": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@minhducsun2002/leb128/-/leb128-1.0.0.tgz", + "integrity": "sha512-eFrYUPDVHeuwWHluTG1kwNQUEUcFjVKYwPkU8z9DR1JH3AW7JtJsG9cRVGmwz809kKtGfwGJj58juCZxEvnI/g==", + "license": "MIT" + }, "node_modules/@next/env": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", @@ -1247,6 +1441,144 @@ "node": ">=5.10" } }, + "node_modules/@solana/buffer-layout-utils": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz", + "integrity": "sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==", + "license": "Apache-2.0", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/web3.js": "^1.32.0", + "bigint-buffer": "^1.1.5", + "bignumber.js": "^9.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@solana/codecs": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs/-/codecs-2.0.0-rc.1.tgz", + "integrity": "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/options": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-core": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-core/-/codecs-core-2.0.0-rc.1.tgz", + "integrity": "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==", + "license": "MIT", + "dependencies": { + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-data-structures": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-data-structures/-/codecs-data-structures-2.0.0-rc.1.tgz", + "integrity": "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-numbers": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-numbers/-/codecs-numbers-2.0.0-rc.1.tgz", + "integrity": "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/codecs-strings": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/codecs-strings/-/codecs-strings-2.0.0-rc.1.tgz", + "integrity": "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "fastestsmallesttextencoderdecoder": "^1.0.22", + "typescript": ">=5" + } + }, + "node_modules/@solana/errors": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/errors/-/errors-2.0.0-rc.1.tgz", + "integrity": "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "commander": "^12.1.0" + }, + "bin": { + "errors": "bin/cli.mjs" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, + "node_modules/@solana/errors/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@solana/errors/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@solana/options": { + "version": "2.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@solana/options/-/options-2.0.0-rc.1.tgz", + "integrity": "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==", + "license": "MIT", + "dependencies": { + "@solana/codecs-core": "2.0.0-rc.1", + "@solana/codecs-data-structures": "2.0.0-rc.1", + "@solana/codecs-numbers": "2.0.0-rc.1", + "@solana/codecs-strings": "2.0.0-rc.1", + "@solana/errors": "2.0.0-rc.1" + }, + "peerDependencies": { + "typescript": ">=5" + } + }, "node_modules/@solana/qr-code-styling": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@solana/qr-code-styling/-/qr-code-styling-1.6.0.tgz", @@ -1256,6 +1588,55 @@ "qrcode-generator": "^1.4.3" } }, + "node_modules/@solana/spl-token": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@solana/spl-token/-/spl-token-0.4.9.tgz", + "integrity": "sha512-g3wbj4F4gq82YQlwqhPB0gHFXfgsC6UmyGMxtSLf/BozT/oKd59465DbnlUK8L8EcimKMavxsVAMoLcEdeCicg==", + "license": "Apache-2.0", + "dependencies": { + "@solana/buffer-layout": "^4.0.0", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/spl-token-group": "^0.0.7", + "@solana/spl-token-metadata": "^0.1.6", + "buffer": "^6.0.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.3" + } + }, + "node_modules/@solana/spl-token-group": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@solana/spl-token-group/-/spl-token-group-0.0.7.tgz", + "integrity": "sha512-V1N/iX7Cr7H0uazWUT2uk27TMqlqedpXHRqqAbVO2gvmJyT0E0ummMEAVQeXZ05ZhQ/xF39DLSdBp90XebWEug==", + "license": "Apache-2.0", + "dependencies": { + "@solana/codecs": "2.0.0-rc.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.3" + } + }, + "node_modules/@solana/spl-token-metadata": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@solana/spl-token-metadata/-/spl-token-metadata-0.1.6.tgz", + "integrity": "sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==", + "license": "Apache-2.0", + "dependencies": { + "@solana/codecs": "2.0.0-rc.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@solana/web3.js": "^1.95.3" + } + }, "node_modules/@solana/web3.js": { "version": "1.95.3", "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.95.3.tgz", @@ -1878,6 +2259,15 @@ "node": ">= 10.0.0" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3075,6 +3465,13 @@ "resolved": "https://registry.npmjs.org/fast-stable-stringify/-/fast-stable-stringify-1.0.0.tgz", "integrity": "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==" }, + "node_modules/fastestsmallesttextencoderdecoder": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", + "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==", + "license": "CC0-1.0", + "peer": true + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -4187,6 +4584,111 @@ "node": ">= 0.8.0" } }, + "node_modules/lighthouse-sdk-legacy": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lighthouse-sdk-legacy/-/lighthouse-sdk-legacy-2.0.0.tgz", + "integrity": "sha512-/nd8Wgf4/Tk/FyJvL9+E7xvKjERY0O91hVmUwzG+LZXt8N5q+dctPP1C12gGXo6ff8B9AQPuAZ2sKToVQjHCnw==", + "license": "MIT", + "dependencies": { + "@metaplex-foundation/umi": "^0.9.1", + "@metaplex-foundation/umi-serializers-core": "^0.8.9", + "@minhducsun2002/leb128": "^1.0.0", + "@solana/web3.js": "1.91.7" + } + }, + "node_modules/lighthouse-sdk-legacy/node_modules/@solana/web3.js": { + "version": "1.91.7", + "resolved": "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.91.7.tgz", + "integrity": "sha512-HqljZKDwk6Z4TajKRGhGLlRsbGK4S8EY27DA7v1z6yakewiUY3J7ZKDZRxcqz2MYV/ZXRrJ6wnnpiHFkPdv0WA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.4", + "@noble/curves": "^1.4.0", + "@noble/hashes": "^1.3.3", + "@solana/buffer-layout": "^4.0.1", + "agentkeepalive": "^4.5.0", + "bigint-buffer": "^1.1.5", + "bn.js": "^5.2.1", + "borsh": "^0.7.0", + "bs58": "^4.0.1", + "buffer": "6.0.3", + "fast-stable-stringify": "^1.0.0", + "jayson": "^4.1.0", + "node-fetch": "^2.7.0", + "rpc-websockets": "^7.5.1", + "superstruct": "^0.14.2" + } + }, + "node_modules/lighthouse-sdk-legacy/node_modules/base-x": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lighthouse-sdk-legacy/node_modules/bs58": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", + "integrity": "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==", + "license": "MIT", + "dependencies": { + "base-x": "^3.0.2" + } + }, + "node_modules/lighthouse-sdk-legacy/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/lighthouse-sdk-legacy/node_modules/rpc-websockets": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/rpc-websockets/-/rpc-websockets-7.11.2.tgz", + "integrity": "sha512-pL9r5N6AVHlMN/vT98+fcO+5+/UcPLf/4tq+WUaid/PPUGS/ttJ3y8e9IqmaWKtShNAysMSjkczuEA49NuV7UQ==", + "license": "LGPL-3.0-only", + "dependencies": { + "eventemitter3": "^4.0.7", + "uuid": "^8.3.2", + "ws": "^8.5.0" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/kozjak" + }, + "optionalDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + } + }, + "node_modules/lighthouse-sdk-legacy/node_modules/superstruct": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-0.14.2.tgz", + "integrity": "sha512-nPewA6m9mR3d6k7WkZ8N8zpTWfenFH3q9pA2PkuiZxINr9DKB2+40wEQf0ixn8VaGuJ78AB6iWOtStI+/4FKZQ==", + "license": "MIT" + }, + "node_modules/lighthouse-sdk-legacy/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -5988,7 +6490,6 @@ "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", - "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/examples/next-js/package.json b/examples/next-js/package.json index 429e932..1277101 100644 --- a/examples/next-js/package.json +++ b/examples/next-js/package.json @@ -10,6 +10,8 @@ }, "dependencies": { "@hookform/resolvers": "^3.6.0", + "@metaplex-foundation/umi-bundle-defaults": "^0.9.2", + "@metaplex-foundation/umi-web3js-adapters": "^0.9.2", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-label": "^2.0.2", @@ -17,12 +19,14 @@ "@radix-ui/react-slot": "^1.0.2", "@solana/actions": "^1.6.4", "@solana/qr-code-styling": "^1.6.0", + "@solana/spl-token": "^0.4.9", "@solana/web3.js": "^1.95.3", "bs58": "^5.0.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "cross-fetch": "^4.0.0", "js-base64": "^3.7.7", + "lighthouse-sdk-legacy": "^2.0.0", "lucide-react": "^0.390.0", "next": "14.2.3", "next-themes": "^0.3.0", diff --git a/examples/next-js/src/app/api/actions/memo/route.ts b/examples/next-js/src/app/api/actions/memo/route.ts index 3d17c0d..9a9236b 100644 --- a/examples/next-js/src/app/api/actions/memo/route.ts +++ b/examples/next-js/src/app/api/actions/memo/route.ts @@ -22,8 +22,12 @@ import { MemoQuerySchema } from "./schema"; // Create memo program instruction import { createMemoInstruction } from "./instruction"; +import { createQueryParser } from "../../utils/validation"; + const headers = createActionHeaders(); +const parseQueryParams = createQueryParser(MemoQuerySchema); + async function handleGet(req: Request): Promise { const requestUrl = new URL(req.url); const baseHref = new URL("/api/actions/memo", requestUrl.origin).toString(); @@ -85,18 +89,6 @@ async function handlePost(req: Request): Promise { }); } -function parseQueryParams(requestUrl: URL) { - const result = MemoQuerySchema.safeParse( - Object.fromEntries(requestUrl.searchParams) - ); - - if (!result.success) { - throw result.error; - } - - return result.data; -} - export const { GET, POST, OPTIONS } = createActionRoutes( { GET: handleGet, diff --git a/examples/next-js/src/app/api/actions/memo/schema.ts b/examples/next-js/src/app/api/actions/memo/schema.ts index 6b54d53..cf5b809 100644 --- a/examples/next-js/src/app/api/actions/memo/schema.ts +++ b/examples/next-js/src/app/api/actions/memo/schema.ts @@ -1,7 +1,14 @@ import { z } from "zod"; +import { publicKeySchema } from "../../utils/validation"; export const MemoQuerySchema = z.object({ - message: z.string().min(1, "Message is required").max(256, "Message must be less than 256 characters"), + message: z.string().min(1, "Message is required"), }); -export type MemoQuery = z.infer; \ No newline at end of file +export type MemoQuery = z.infer; + +export const MemoBodySchema = z.object({ + account: publicKeySchema, +}); + +export type MemoBody = z.infer; \ No newline at end of file diff --git a/examples/next-js/src/app/api/actions/paymaster/route.ts b/examples/next-js/src/app/api/actions/paymaster/route.ts new file mode 100644 index 0000000..1784626 --- /dev/null +++ b/examples/next-js/src/app/api/actions/paymaster/route.ts @@ -0,0 +1,125 @@ +import { + ActionGetResponse, + ActionPostRequest, + ActionPostResponse, + createActionHeaders, + createPostResponse, +} from "@solana/actions"; +import { createActionRoutes } from "../../utils/action-handler"; +import { loadPaymasterKeypair } from "../../utils/keypair"; +import { createQueryParser } from "../../utils/validation"; +import { GenericTransactionExtensionSchema } from "./schema"; +import { + AddressLookupTableAccount, + Message, + MessageV0, + Transaction, + TransactionMessage, + VersionedMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { getConnection } from "../../utils/connection"; + +const headers = createActionHeaders(); + +async function handleGet(req: Request): Promise { + const requestUrl = new URL(req.url); + const paymaster = loadPaymasterKeypair(); + + const baseHref = new URL( + `/api/actions/paymaster`, + requestUrl.origin, + ).toString(); + + return { + type: "action", + title: "Actions Example - Paymaster", + icon: new URL("/solana_devs.jpg", requestUrl.origin).toString(), + description: `Have ${paymaster.publicKey.toBase58()} cover the tx fee`, + label: "Paymaster", + links: { + actions: [ + { + label: "Cover Fee", + href: `${baseHref}?blink={blink}`, + parameters: [ + { + type: "url", + name: "blink", + label: "Blink URL", + required: true, + }, + ], + }, + ], + }, + }; +} + +const parseQueryParams = createQueryParser(GenericTransactionExtensionSchema); + +// http://localhost:3000/api/actions/transfer-sol?amount=0.01&to=67ZiM1TRqPFR5s2Jz1z4d6noHHBRRzt1Te6xbWmPgYF7 +// http://localhost:3000/api/actions/transfer-sol?amount=0.001&to=6Le7uLy8Y2JvCq5x5huvF3pSQBvP1Y6W325wNpFz4s4u +// http://localhost:3000/api/actions/transfer-spl?&amount=1&to=67ZiM1TRqPFR5s2Jz1z4d6noHHBRRzt1Te6xbWmPgYF7&mint=CzLSujWBLFsSjncfkh59rUFqvafWcY5tzedWJSuypump + +async function handlePost(req: Request): Promise { + const requestUrl = new URL(req.url); + const paymaster = loadPaymasterKeypair(); + const { blink } = parseQueryParams(requestUrl); + + const body: ActionPostRequest = await req.json(); + + const txResponse = await fetch(blink, { + method: "POST", + body: JSON.stringify({ account: body.account }), + }); + + const txResponseBody: ActionPostResponse = await txResponse.json(); + const tx = VersionedTransaction.deserialize( + Buffer.from(txResponseBody.transaction, "base64"), + ); + + // hydrate the message's instructions using the static account keys and lookup tables + const connection = getConnection(); + const LUTs = ( + await Promise.all( + tx.message.addressTableLookups.map((acc) => + connection.getAddressLookupTable(acc.accountKey), + ), + ) + ) + .map((lut) => lut.value) + .filter((val) => val !== null) as AddressLookupTableAccount[]; + + // if we need to get all accounts + // const allAccs = tx.message.getAccountKeys({ addressLookupTableAccounts: LUTs }) + // .keySegments().reduce((acc, cur) => acc.concat(cur), []); + + const txMessage = TransactionMessage.decompile(tx.message, { + addressLookupTableAccounts: LUTs, + }); + + // Modify the message to use the paymaster as the payer + txMessage.payerKey = paymaster.publicKey; + txMessage.recentBlockhash = (await connection.getLatestBlockhash()).blockhash; + + const finalTx = new VersionedTransaction(txMessage.compileToV0Message()); + finalTx.sign([paymaster]); + + return createPostResponse({ + fields: { + transaction: finalTx, + message: `Covered fee for "${Buffer.from(tx.serialize()).toString( + "base64", + )}"`, + }, + }); +} + +export const { GET, POST, OPTIONS } = createActionRoutes( + { + GET: handleGet, + POST: handlePost, + }, + headers, +); diff --git a/examples/next-js/src/app/api/actions/paymaster/schema.ts b/examples/next-js/src/app/api/actions/paymaster/schema.ts new file mode 100644 index 0000000..e50a163 --- /dev/null +++ b/examples/next-js/src/app/api/actions/paymaster/schema.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; +import { publicKeySchema, numberFromStringSchema } from "../../utils/validation"; + +export const GenericTransactionExtensionSchema = z.object({ + blink: z.string(), +}); + +export type GenericTransactionExtension = z.infer; \ No newline at end of file diff --git a/examples/next-js/src/app/api/actions/transfer-sol/route.ts b/examples/next-js/src/app/api/actions/transfer-sol/route.ts index 44d2b6f..f6a0ce8 100644 --- a/examples/next-js/src/app/api/actions/transfer-sol/route.ts +++ b/examples/next-js/src/app/api/actions/transfer-sol/route.ts @@ -15,6 +15,7 @@ import { SystemProgram, Transaction, } from "@solana/web3.js"; +import { createQueryParser } from "../../utils/validation"; import { TransferSolQuerySchema } from "./schema"; import { createActionRoutes } from "../../utils/action-handler"; import { getConnection } from "../../utils/connection"; @@ -22,6 +23,8 @@ import { getConnection } from "../../utils/connection"; // create the standard headers for this route (including CORS) const headers = createActionHeaders(); +const parseQueryParams = createQueryParser(TransferSolQuerySchema); + async function handleGet(req: Request): Promise { const requestUrl = new URL(req.url); const baseHref = new URL( @@ -79,7 +82,8 @@ async function handlePost(req: Request): Promise { lamports: amount * LAMPORTS_PER_SOL, }); - const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); + const { blockhash, lastValidBlockHeight } = + await connection.getLatestBlockhash(); const transaction = new Transaction({ feePayer: account, @@ -95,22 +99,10 @@ async function handlePost(req: Request): Promise { }); } -function parseQueryParams(requestUrl: URL) { - const result = TransferSolQuerySchema.safeParse( - Object.fromEntries(requestUrl.searchParams) - ); - - if (!result.success) { - throw result.error; - } - - return result.data; -} - export const { GET, POST, OPTIONS } = createActionRoutes( { GET: handleGet, POST: handlePost, }, - headers + headers, ); diff --git a/examples/next-js/src/app/api/actions/transfer-sol/schema.ts b/examples/next-js/src/app/api/actions/transfer-sol/schema.ts index 65818cd..bdf1e79 100644 --- a/examples/next-js/src/app/api/actions/transfer-sol/schema.ts +++ b/examples/next-js/src/app/api/actions/transfer-sol/schema.ts @@ -1,15 +1,13 @@ import { z } from "zod"; +import { PublicKey } from "@solana/web3.js"; import { DEFAULT_SOL_AMOUNT } from "./const"; -import { publicKeySchema, numberFromStringSchema } from "../../utils/validation"; +import { numberFromStringSchema, publicKeySchema } from "../../utils/validation"; +// Define the input type (what comes from URL params) export const TransferSolQuerySchema = z.object({ to: publicKeySchema, - amount: numberFromStringSchema({ - min: 0, - description: "SOL amount" - }) - .optional() - .default(DEFAULT_SOL_AMOUNT.toString()), + amount: numberFromStringSchema({ min: 0 }), }); +// Type representing the parsed and transformed data export type TransferSolQuery = z.infer; \ No newline at end of file diff --git a/examples/next-js/src/app/api/actions/transfer-spl/route.ts b/examples/next-js/src/app/api/actions/transfer-spl/route.ts new file mode 100644 index 0000000..cee506b --- /dev/null +++ b/examples/next-js/src/app/api/actions/transfer-spl/route.ts @@ -0,0 +1,99 @@ +import { ActionGetResponse, ActionPostRequest, ActionPostResponse, createActionHeaders, createPostResponse } from "@solana/actions"; +import { PublicKey, Transaction } from "@solana/web3.js"; +import { getConnection } from "../../utils/connection"; +import { createActionRoutes } from "../../utils/action-handler"; +import { TransferSplQuerySchema } from "./schema"; +import { createQueryParser } from "../../utils/validation"; +import { createTransferInstruction, getAssociatedTokenAddressSync, getMint } from "@solana/spl-token"; + +// create the standard headers for this route (including CORS) +const headers = createActionHeaders(); + +const parseQueryParams = createQueryParser(TransferSplQuerySchema); + +async function handleGet(req: Request): Promise { + const requestUrl = new URL(req.url); + const result = TransferSplQuerySchema.safeParse( + Object.fromEntries(requestUrl.searchParams) + ); + + const baseHref = new URL( + `/api/actions/transfer-spl?`, + requestUrl.origin, + ).toString(); + + return { + type: "action", + title: "Actions Example - Transfer SPL Token", + icon: new URL("/solana_devs.jpg", requestUrl.origin).toString(), + description: "Transfer SOL to another Solana wallet", + label: "Transfer", + links: { + actions: [ + { + label: "Send SPL Token", + href: `${baseHref}&amount={amount}&to={to}&mint={mint}`, + parameters: [ + { + name: "amount", + label: "Enter the amount of SPL tokens to send", + required: true, + }, + { + name: "to", + label: "Enter the address to send SPL tokens", + required: true, + }, + { + name: "mint", + label: "Enter the SPL token mint address", + required: true, + }, + ], + }, + ], + }, + }; +} + +async function handlePost(req: Request): Promise { + const requestUrl = new URL(req.url); + const { to: toPubkey, mint, amount } = parseQueryParams(requestUrl); + + const body: ActionPostRequest = await req.json(); + const account = new PublicKey(body.account); + + const connection = getConnection(); + // get ATAs + const mintInfo = await getMint(connection, mint, "confirmed"); + const sourceAta = getAssociatedTokenAddressSync(mint, account); + const destinationAta = getAssociatedTokenAddressSync(mint, toPubkey); + // get the true amount of tokens to transfer + const amountToTransfer = BigInt(amount) * BigInt(10 ** mintInfo.decimals); + const transferSplInstruction = createTransferInstruction(sourceAta, destinationAta, account, amountToTransfer); + + const { blockhash, lastValidBlockHeight } = + await connection.getLatestBlockhash(); + + const transaction = new Transaction({ + feePayer: account, + blockhash, + lastValidBlockHeight, + }).add(transferSplInstruction); + + return createPostResponse({ + fields: { + transaction, + message: `Send ${amount} ${mint.toBase58()} to ${toPubkey.toBase58()}`, + }, + }); +} + + +export const { GET, POST, OPTIONS } = createActionRoutes( + { + GET: handleGet, + POST: handlePost, + }, + headers, +); diff --git a/examples/next-js/src/app/api/actions/transfer-spl/schema.ts b/examples/next-js/src/app/api/actions/transfer-spl/schema.ts new file mode 100644 index 0000000..704fada --- /dev/null +++ b/examples/next-js/src/app/api/actions/transfer-spl/schema.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; +import { publicKeySchema, numberFromStringSchema } from "../../utils/validation"; + +export const TransferSplQuerySchema = z.object({ + to: publicKeySchema, + mint: publicKeySchema, + amount: numberFromStringSchema({ + min: 1, + description: "SPL token amount" + }) +}); + +export type TransferSolQuery = z.infer; \ No newline at end of file diff --git a/examples/next-js/src/app/api/utils/connection.ts b/examples/next-js/src/app/api/utils/connection.ts index c1ebbe7..173498d 100644 --- a/examples/next-js/src/app/api/utils/connection.ts +++ b/examples/next-js/src/app/api/utils/connection.ts @@ -28,7 +28,7 @@ export type ConnectionConfig = { * const connection = getConnection({ commitment: 'confirmed' }); */ export function getConnection(config: ConnectionConfig = {}): Connection { - const endpoint = config.endpoint || process.env.SOLANA_RPC || clusterApiUrl("devnet"); + const endpoint = config.endpoint || process.env.SOLANA_RPC || clusterApiUrl("mainnet-beta"); return new Connection(endpoint, { commitment: config.commitment || 'confirmed', diff --git a/examples/next-js/src/app/api/utils/keypair.ts b/examples/next-js/src/app/api/utils/keypair.ts new file mode 100644 index 0000000..663bfcc --- /dev/null +++ b/examples/next-js/src/app/api/utils/keypair.ts @@ -0,0 +1,43 @@ +import { Keypair } from "@solana/web3.js"; +import { Buffer } from "buffer"; + +/** + * Loads a Keypair from a base58 or byte array secret key string + * + * @param secretKeyString - Either a base58 string or a comma-separated byte array string + * @returns Keypair instance + * + * Usage: + * const kp = loadKeypairFromString(process.env.SOME_SECRET_KEY) + */ +export function loadKeypairFromString(secretKeyString: string): Keypair { + try { + // First try parsing as a byte array + if (secretKeyString.includes(',')) { + const bytes = secretKeyString.substring(1, secretKeyString.length - 1).split(',').map(s => parseInt(s)); + return Keypair.fromSecretKey(new Uint8Array(bytes)); + } + + // Then try as base64 + return Keypair.fromSecretKey( + Buffer.from(secretKeyString, 'base64') + ); + } catch (err) { + throw new Error('Invalid secret key format. Must be base58 or byte array.'); + } +} + +/** + * Loads the paymaster keypair from environment variables + * + * @returns Keypair for the paymaster account + * @throws Error if PAYMASTER_SECRET_KEY is not set + */ +export function loadPaymasterKeypair(): Keypair { + const secretKey = process.env.PAYMASTER_SECRET_KEY; + if (!secretKey) { + throw new Error('PAYMASTER_SECRET_KEY environment variable is not set'); + } + + return loadKeypairFromString(secretKey); +} \ No newline at end of file diff --git a/examples/next-js/src/app/api/utils/validation.ts b/examples/next-js/src/app/api/utils/validation.ts index 52a9d1e..82c3c2a 100644 --- a/examples/next-js/src/app/api/utils/validation.ts +++ b/examples/next-js/src/app/api/utils/validation.ts @@ -3,7 +3,7 @@ import { PublicKey } from "@solana/web3.js"; /** * Zod Schema Validation Utilities - * + * * Zod is a TypeScript-first schema validation library that allows us to: * 1. Define the shape and requirements of our data * 2. Parse and transform input data @@ -13,13 +13,13 @@ import { PublicKey } from "@solana/web3.js"; /** * Schema for validating Solana public keys - * + * * This schema: * 1. Accepts a string input * 2. Attempts to create a Solana PublicKey from the string * 3. Returns the PublicKey object if valid * 4. Provides a clear error message if invalid - * + * * Usage: * const result = publicKeySchema.safeParse("ABC123...") * if (result.success) { @@ -40,23 +40,23 @@ export const publicKeySchema = z.string().transform((val, ctx) => { /** * Creates a schema for parsing and validating numeric values from strings - * + * * Features: * 1. Converts string input to number * 2. Validates the number is not NaN * 3. Optionally checks minimum and maximum bounds * 4. Provides contextual error messages - * + * * @param options Configuration object for validation * @param options.min Minimum allowed value (inclusive) * @param options.max Maximum allowed value (inclusive) * @param options.description Human-readable description of the field for error messages - * + * * Usage: - * const amountSchema = numberFromStringSchema({ - * min: 0, - * max: 100, - * description: "token amount" + * const amountSchema = numberFromStringSchema({ + * min: 0, + * max: 100, + * description: "token amount" * }) */ export const numberFromStringSchema = (options?: { @@ -65,27 +65,33 @@ export const numberFromStringSchema = (options?: { description?: string; }) => { // First transform string to number and validate it's a valid number - let schema = z - .string() - .transform((val, ctx) => { - const parsed = Number(val); - if (isNaN(parsed)) { - ctx.addIssue({ - code: z.ZodIssueCode.custom, - message: `Invalid number format${options?.description ? ` for ${options.description}` : ''}`, - }); - return z.NEVER; - } - return parsed; - }); + let schema = z.string().transform((val, ctx) => { + const parsed = Number(val); + if (isNaN(parsed)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Invalid number format${ + options?.description ? ` for ${options.description}` : "" + }`, + }); + return z.NEVER; + } + return parsed; + }); // Then apply any min/max constraints to the resulting number let baseNumberSchema = z.number(); if (options?.min !== undefined) { - baseNumberSchema = baseNumberSchema.min(options.min, `Value must be greater than ${options.min}`); + baseNumberSchema = baseNumberSchema.min( + options.min, + `Value must be greater than ${options.min}`, + ); } if (options?.max !== undefined) { - baseNumberSchema = baseNumberSchema.max(options.max, `Value must be less than ${options.max}`); + baseNumberSchema = baseNumberSchema.max( + options.max, + `Value must be less than ${options.max}`, + ); } // Chain the string->number transform with the number validation @@ -96,4 +102,38 @@ export const numberFromStringSchema = (options?: { * Type helper for getting the type of a validated public key * This will resolve to the Solana PublicKey type */ -export type ZodPublicKey = z.infer; \ No newline at end of file +export type ZodPublicKey = z.infer; + +/** + * Creates a function to parse URL search parameters using a Zod schema + * + * Features: + * 1. Type-safe parsing of URL parameters + * 2. Automatic error handling + * 3. Schema-driven validation + * 4. Consistent error messages + * + * @param schema The Zod schema to validate against + * @returns A function that parses URL search params using the schema + * + * Usage: + * const parseParams = createQueryParser(MyQuerySchema); + * const params = parseParams(new URL(request.url)); + */ +export function createQueryParser< + TOutput, + TTypeDef extends z.ZodTypeDef, + IInput, +>(schema: z.ZodSchema) { + return function parseQueryParams(requestUrl: URL): TOutput { + const result = schema.safeParse( + Object.fromEntries(requestUrl.searchParams), + ); + + if (!result.success) { + throw result.error; + } + + return result.data; + }; +}