diff --git a/contracts/sources/storytime/story_nft.move b/contracts/sources/storytime/story_nft.move index 3c67d28..2b7b062 100644 --- a/contracts/sources/storytime/story_nft.move +++ b/contracts/sources/storytime/story_nft.move @@ -79,7 +79,7 @@ module storytime::story_nft { } // ===== Entrypoints ===== - + #[lint_allow(self_transfer)] public fun mint(name: String, image_url: String, ctx: &mut TxContext) { let sender = tx_context::sender(ctx); let id = object::new(ctx); @@ -101,6 +101,26 @@ module storytime::story_nft { transfer::public_transfer(nft, sender); } + public fun mint_to(to: address, name: String, image_url: String, ctx: &mut TxContext) { + let id = object::new(ctx); + let chapters = vector[]; + let nft = StoryNFT { + id, + name, + image_url, + chapters, + auther: to + }; + + event::emit(NFTMinted { + object_id: object::id(&nft), + creator: to, + name: nft.name, + }); + + transfer::public_transfer(nft, to); + } + /// Transfer `nft` to `recipient` public fun transfer( nft: StoryNFT, recipient: address, _: &mut TxContext diff --git a/contracts/src/run.ts b/contracts/src/run.ts index d3ad7c9..0ef6c88 100644 --- a/contracts/src/run.ts +++ b/contracts/src/run.ts @@ -28,24 +28,6 @@ async function main() { ], }); - // const [coin] = txb.splitCoins(txb.gas, [100]); - - // transfer the split coin to a specific address - // txb.transferObjects([coin], '0x380254779600ed29cb70c917255b084d12b5a760c4dadeceb8f7673d0fc99d1d'); - - // const bytes = await txb.build(); - // const serializedSignature = (await keypair.signTransactionBlock(bytes)) - // .signature; - - // // verify the signature locally - // const verified = await keypair - // .getPublicKey() - // .verifyTransactionBlock(bytes, serializedSignature) - - // if (!verified) { - // throw new Error('Signature verification failed'); - // } - // define sui client for the desired network. const client = new SuiClient({ url: getFullnodeUrl('devnet') }); const result = await client.signAndExecuteTransactionBlock({ diff --git a/functions/.gitignore b/functions/.gitignore index b65ca33..029c5f6 100644 --- a/functions/.gitignore +++ b/functions/.gitignore @@ -8,3 +8,4 @@ typings/ # Node.js dependency directory node_modules/ lib/ +src/credentials/admin-key.json diff --git a/functions/package.json b/functions/package.json index b2ea716..79b6f05 100644 --- a/functions/package.json +++ b/functions/package.json @@ -15,7 +15,10 @@ "main": "lib/index.js", "dependencies": { "@google-cloud/functions-framework": "^3.3.0", + "@mysten/bcs": "^0.9.0", + "@mysten/sui.js": "^0.48.0", "@types/express": "^4.17.21", + "dotenv": "^16.3.1", "express": "^4.18.2", "firebase-admin": "^11.11.1", "firebase-functions": "^4.3.1", @@ -30,4 +33,4 @@ "typescript": "^5.3.3" }, "private": true -} +} \ No newline at end of file diff --git a/functions/pnpm-lock.yaml b/functions/pnpm-lock.yaml index e7827fc..fc5841f 100644 --- a/functions/pnpm-lock.yaml +++ b/functions/pnpm-lock.yaml @@ -8,9 +8,18 @@ dependencies: '@google-cloud/functions-framework': specifier: ^3.3.0 version: 3.3.0 + '@mysten/bcs': + specifier: ^0.9.0 + version: 0.9.0 + '@mysten/sui.js': + specifier: ^0.48.0 + version: 0.48.0 '@types/express': specifier: ^4.17.21 version: 4.17.21 + dotenv: + specifier: ^16.3.1 + version: 16.3.1 express: specifier: ^4.18.2 version: 4.18.2 @@ -801,6 +810,37 @@ packages: lodash: 4.17.21 optional: true + /@mysten/bcs@0.9.0: + resolution: {integrity: sha512-h56essa8oSS4/J0Dby8k8stMoOSt+QZIEIeZNtgTOWh9HeV69yFg2BUg/+Rk7jzfWzvUmw9lFyKNipXcD5QOTw==} + dependencies: + bs58: 5.0.0 + dev: false + + /@mysten/sui.js@0.48.0: + resolution: {integrity: sha512-kGeV5F3IYThiliVbIou3YOOGAr8TdNyub4hGY2gevnphFQF8SukwQk8cyzuEEsAVSDUt0rsA9xxpncSgX+RjMA==} + engines: {node: '>=16'} + dependencies: + '@mysten/bcs': 0.9.0 + '@noble/curves': 1.3.0 + '@noble/hashes': 1.3.3 + '@scure/bip32': 1.3.3 + '@scure/bip39': 1.2.1 + '@suchipi/femver': 1.0.0 + superstruct: 1.0.3 + tweetnacl: 1.0.3 + dev: false + + /@noble/curves@1.3.0: + resolution: {integrity: sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==} + dependencies: + '@noble/hashes': 1.3.3 + dev: false + + /@noble/hashes@1.3.3: + resolution: {integrity: sha512-V7/fPHgl+jsVPXqqeOzT8egNj2iBIVt+ECeMMG8TdcnTikP3oaBtUVqpT/gYCR68aEBJSF+XbYUxStjbFMqIIA==} + engines: {node: '>= 16'} + dev: false + /@protobufjs/aspromise@1.1.2: resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} @@ -834,6 +874,25 @@ packages: /@protobufjs/utf8@1.1.0: resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + /@scure/base@1.1.4: + resolution: {integrity: sha512-wznebWtt+ejH8el87yuD4i9xLSbYZXf1Pe4DY0o/zq/eg5I0VQVXVbFs6XIM0pNVCJ/uE3t5wI9kh90mdLUxtw==} + dev: false + + /@scure/bip32@1.3.3: + resolution: {integrity: sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==} + dependencies: + '@noble/curves': 1.3.0 + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.4 + dev: false + + /@scure/bip39@1.2.1: + resolution: {integrity: sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==} + dependencies: + '@noble/hashes': 1.3.3 + '@scure/base': 1.1.4 + dev: false + /@sinclair/typebox@0.27.8: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true @@ -855,6 +914,10 @@ packages: '@sinonjs/commons': 3.0.0 dev: true + /@suchipi/femver@1.0.0: + resolution: {integrity: sha512-bprE8+K5V+DPX7q2e2K57ImqNBdfGHDIWaGI5xHxZoxbKOuQZn4wzPiUxOAHnsUr3w3xHrWXwN7gnG/iIuEMIg==} + dev: false + /@szmarczak/http-timer@4.0.6: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -1311,6 +1374,10 @@ packages: resolution: {integrity: sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==} dev: false + /base-x@4.0.0: + resolution: {integrity: sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} requiresBuild: true @@ -1376,6 +1443,12 @@ packages: update-browserslist-db: 1.0.13(browserslist@4.22.2) dev: true + /bs58@5.0.0: + resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} + dependencies: + base-x: 4.0.0 + dev: false + /bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} dependencies: @@ -1702,6 +1775,11 @@ packages: md5: 2.3.0 dev: false + /dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dev: false + /duplexify@4.1.2: resolution: {integrity: sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==} requiresBuild: true @@ -4011,6 +4089,11 @@ packages: requiresBuild: true optional: true + /superstruct@1.0.3: + resolution: {integrity: sha512-8iTn3oSS8nRGn+C2pgXSKPI3jmpm6FExNazNpjvqS6ZUJQCej3PUXEKM8NjHBOs54ExM+LPW/FBRhymrdcCiSg==} + engines: {node: '>=14.0.0'} + dev: false + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -4099,6 +4182,10 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + dev: false + /type-check@0.3.2: resolution: {integrity: sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==} engines: {node: '>= 0.8.0'} diff --git a/functions/src/credentials/admin-key.json b/functions/src/credentials/admin-key.json deleted file mode 100644 index 9c325f1..0000000 --- a/functions/src/credentials/admin-key.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "service_account", - "project_id": "storytime-web3", - "private_key_id": "c948532af7977cea6405e645825b2bdfd2c3e3d6", - "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDClnYzEADTVtoI\nKEt2gnpEpEKQLVOVWao7Z2QNalKtuEtZb3GqL5tFDWi5d0CzejoqADQTq30bIV/m\narYk5JnCDG8NlJ7prcJc2t5KfrKOX/ReVdy7Jn1pCy3wv7S5qChEYNOojzDufEmb\nBFclFUUGlFkZO5lgQHKQfXUr0j7khL0Z0d6QxhT8D4tEqLjCYz+ZRlPvHdEvP9kf\naBc3e/amQNXwwyhXidtOPwYsVkIBTmHBtUYXx4gcTJrOwumHwhJYQcRpgfjtt2yO\ncARYRlADtArJY+SXY6laSD9KNZfJ3CWKZtGcbtdMCOcRdMlD5hIOS7HH02Iy7wcv\nHuBckoW/AgMBAAECggEAGLTkUXZyNw3w2sFMV5dDMVej20zjWclhO8VUZv79fBuJ\ndv5snF21ZW6e7JGPha+la/N2U/BduaX4Aw3Ajm2iIjPUSTv6HhSBuFV9N6XfmQPm\nR1tijowEDLmD00IcDx337ZACaoVdLU1a8Sj7bEoc517K81EQbTAG+9aaG9QdnS5k\nD0HyHbcNneT6lQrjv+3iMh7zGwzmSWSxD81fKhrGalAB97OZwdiwZSQKp2icySRU\nsnhk0FFsggQ2KHaX26zrp0s9VSUGMvD82xDymSU6YWwrIlaC2P1lDy72bo2EaeRS\nh9qZKld7e1vFNUuKaZhiHPNF8CgEhq9ztVvOVgQW3QKBgQDmJqhDyoWSg7Qr1rba\nOs4UOigQnyRrd8vwarxyvm97tUTbzAeyaKvfxSNlxxRnGowh2Gm92sX7togJ+5w1\nnTBjxQjvvFLaTEsUdRQUPH7UcEbG71+YJpS82Izznc4UNYP3GLlsljQ20eQ+2PM/\nrHc4zm+qZb1MVoXiduCjKvU5SwKBgQDYcUh7J0TiKTDTWyyHagDVIImz0yfY3FXf\noqov8kJemApHgXSkH4NjSQV7bDpBWytt+kZEjIlLMXCx9/gUNRQIdAzX4n5qeOZr\nohRP6Fh7atycnlQQ+ifrjPcb+3OEuccxog+6nTqbTaJMig0Pa3yJqR1psIHlOxSF\nzeFzgSYw3QKBgQDRQmzT1pxEb0OuaHexTK57bbVoB4rRvAbN/f6CReyPim2le4m0\nCb/coh5hN/WxGU44p9DdMsG48GoYyZFqnhWVYV2SvKSIn73UR++NxsN63Os9jgBZ\nExtB6ZOfHeh2L4JhdTWDKb8n6QeirRfe2S09lVWqlP7dHf51vqjZMwHsqQKBgQC/\no9YETOm9wbDcgs6ze1UPYAbstJqEddqG73T2jO7c6Iu5clL/enOP9jinZlVSRtH2\nR3HuAe0SKc1ZlnAOHE0HixFQGNfLmA6U4GZRtiyZ4i1BcyKGAahU9HRbT1GiBAft\n10tL9SjOF3gLgvQ7YfVbweQsDz+D4sKyEm97IK/a9QKBgDNz3sj+qkbgMn0sW2Fw\n3o5h9oWvEk2Zvvpo8+g/w3xEgoS2RbboqsCAN+JNw/fKecFrlZA97xVgd6fQjZNb\nfnqPRx3uHFKQwaOGaEAc5yTA5JFDSBdeRO5qXrHnPTyqNbwk4hzIAz+LcSn0WJgW\neG0OLLjO1ZadmECODGEHlp8U\n-----END PRIVATE KEY-----\n", - "client_email": "firebase-adminsdk-i9jvg@storytime-web3.iam.gserviceaccount.com", - "client_id": "112791080690462614850", - "auth_uri": "https://accounts.google.com/o/oauth2/auth", - "token_uri": "https://oauth2.googleapis.com/token", - "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", - "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-i9jvg%40storytime-web3.iam.gserviceaccount.com", - "universe_domain": "googleapis.com" -} diff --git a/functions/src/gaslessMint.ts b/functions/src/gaslessMint.ts new file mode 100644 index 0000000..c0cf26d --- /dev/null +++ b/functions/src/gaslessMint.ts @@ -0,0 +1,58 @@ +import { fromHEX } from "@mysten/bcs"; +import { SuiClient, getFullnodeUrl } from "@mysten/sui.js/client"; +import { Ed25519Keypair } from "@mysten/sui.js/keypairs/ed25519"; +import { TransactionBlock } from "@mysten/sui.js/transactions"; +import dotenv from "dotenv"; +import { CloudFunctionsTypeWithUid } from "./handlersType"; +dotenv.config(); + +export const gaslessMint: CloudFunctionsTypeWithUid["gaslessMint"] = async ( + to: string, + title: string, + imageUrl: string +): Promise<{ objectId: string }> => { + const key = process.env.SUI_PRIVATE_KEY; + const keypair = Ed25519Keypair.fromSecretKey(fromHEX(key!), { + skipValidation: false, + }); + + const pk = keypair.getPublicKey(); + const sender = pk.toSuiAddress(); + + // create an example transaction block. + const txb = new TransactionBlock(); + txb.setSender(sender); + txb.setGasBudget(5_000_000); + + txb.moveCall({ + target: `0x84abd49cedd1ddf02ab8e48c167df180b047cdd1e4c07d97434e382658dfffe5::story_nft::mint_to`, + arguments: [txb.pure.address(to), txb.pure(title), txb.pure(imageUrl)], + }); + + // define sui client for the desired network. + const client = new SuiClient({ url: getFullnodeUrl("devnet") }); + const result = await client.signAndExecuteTransactionBlock({ + signer: keypair, + transactionBlock: txb, + }); + + console.log(result); + const transactionBlock = await client.waitForTransactionBlock({ + digest: result.digest, + options: { + showEvents: true, + showEffects: true, + }, + }); + + console.log(transactionBlock); + + console.log(`event: ${JSON.stringify(transactionBlock.events, null, 2)}`); + + const objectId = (transactionBlock.events?.[0]?.parsedJson as any) + .object_id as string; + + return { + objectId, + }; +}; diff --git a/functions/src/index.ts b/functions/src/index.ts index 7121560..922b204 100644 --- a/functions/src/index.ts +++ b/functions/src/index.ts @@ -15,6 +15,7 @@ import admin from "firebase-admin"; import adminKey from "./credentials/admin-key.json"; import { CloudFunctionsTypeWithUid } from "./handlersType"; import { getStory } from "./getStory"; +import { gaslessMint } from "./gaslessMint"; export * from "./serve"; @@ -27,6 +28,7 @@ admin.initializeApp({ const handlers: CloudFunctionsTypeWithUid = { generateImage, getStory, + gaslessMint, }; export const execute = onCall( diff --git a/model/functions.d.ts b/model/functions.d.ts index f04f8a1..1c0107c 100644 --- a/model/functions.d.ts +++ b/model/functions.d.ts @@ -3,4 +3,10 @@ export type CloudFunctionsType = { prompt: string ): Promise<{ image_url: string; revised_prompt: string }>; getStory(id: string): Promise<{ id: string; story: string }>; + + gaslessMint( + to: string, + title: string, + imageUrl: string + ): Promise<{ objectId: string }>; };