diff --git a/ARCs/arc-0047.md b/ARCs/arc-0047.md index 4e440e1bd..82af0288a 100644 --- a/ARCs/arc-0047.md +++ b/ARCs/arc-0047.md @@ -81,6 +81,23 @@ All values **MUST** be validated to ensure they are encoded properly. This inclu To enable unique identification of a description, clients **MUST** calculate the SHA256 hash of the JSON description canonicalized in accordance with RFC 8785. +### WalletConnect Method + +For wallets to support this ARC, they need to supprt the a `algo_templatedLsig` method. + +The method expects three parameters described by the interface below + +```ts +interface TemplatedLsigParams { + /** The canoncalized ARC47 templated lsig JSON as described in this ARC */ + arc47: string + /** The values of the templated variables, if there are any */ + values?: {[variable: string]: string | number} + /** The hash of the expected program. Wallets should compile the lsig with the given values to verify the program hash matches */ + hash: string +} +``` + ## Rationale This provides a way for frontends to clearly display to the user what is being signed when signing a logic signature. @@ -99,30 +116,13 @@ N/A ## Reference Implementation -### Description -```json -{ - "name": "Address Opt-In", - "description": "This program allows a given address to opt-in the signer to any asset provided it's approved by the associated application", - "program": "I3ByYWdtYSB2ZXJzaW9uIDgKI2RlZmluZSBNYXN0ZXJBcHBDYWxsIGxvYWQgMAoKLy8gU2F2ZSBNYXN0ZXJBcHBDYWxsCnR4biBHcm91cEluZGV4CmludCAxCisKc3RvcmUgMAoKLy8gVmVyaWZ5IGFtb3VudCBpcyAwCnR4biBBc3NldEFtb3VudAppbnQgMAo9PQphc3NlcnQKCi8vIFZlcmlmeSBzZW5kZXIgPT0gcmVjZWl2ZXIKdHhuIEFzc2V0UmVjZWl2ZXIKdHhuIFNlbmRlcgo9PQphc3NlcnQKCi8vIFZlcmlmeSBmZWUgaXMgMCAoY292ZXJlZCBieSBzZW5kZXIpCnR4biBGZWUKaW50IDAKPT0KYXNzZXJ0CgovLyBWZXJpZnkgYXNzZXRDbG9zZVRvIGlzIG5vdCBzZXQKdHhuIEFzc2V0Q2xvc2VUbwpnbG9iYWwgWmVyb0FkZHJlc3MKPT0KYXNzZXJ0CgovLyBWZXJpZnkgY2FsbGVkIGF0b21pY2FsbHkgd2l0aCBtYXN0ZXIgYXBwCk1hc3RlckFwcENhbGwKZ3R4bnMgQXBwbGljYXRpb25JRAppbnQgVE1QTF9ERUxFR0FURURfT1BUSU5fQVBQX0lECj09CmFzc2VydAoKLy8gVmVyaWZ5IHRoZSBjb3JyZWN0IG1ldGhvZCBpcyBiZWluZyBjYWxsZWQKTWFzdGVyQXBwQ2FsbApndHhuc2EgQXBwbGljYXRpb25BcmdzIDAKbWV0aG9kICJhZGRyZXNzT3B0SW4ocGF5LGF4ZmVyKXZvaWQiCj09CmFzc2VydAoKLy8gVmVyaWZ5IHRoZSBzZW5kZXIgaXMgdGhlIGNvcnJlY3QgYWRkcmVzcwpNYXN0ZXJBcHBDYWxsCmd0eG5zIFNlbmRlcgphZGRyIFRNUExfQVVUSE9SSVpFRF9BRERSRVNTCj09", - "variables": { - [ - { - "variable": "TMPL_DELEGATED_OPTIN_APP_ID", - "name": "Delegated opt-in app ID", - "type": "application", - "description": "The ID of the application that will be used for verifying opt-ins" - }, - { - "variable": "TMPL_AUTHORIZED_ADDRESS", - "name": "Authorized address", - "type": "address", - "description": "The address that will be allowed to opt the signer into assets provided it's approved by the associated application" - } - ] - } -} -``` +A reference implementation can be found in [../assets/arc-0047](../assets/arc-0047/). + +[lsig.teal](../assets/arc-0047/lsig.teal) contains the templated TEAL code for a logic signature that allows payments of a specific amount every 25,000 blocks. + +[dapp.ts](../assets/arc-0047/dapp.ts) contains a TypeScript script showcasing how a dapp would form a wallet connect request for a templated logic signature. + +[wallet.ts](../assets/arc-0047/wallet.ts) contains a TypeScript script showcasing how a wallet would handle a request for signing a templated logic signature. ### String Variables diff --git a/assets/arc-0047/.gitignore b/assets/arc-0047/.gitignore new file mode 100644 index 000000000..f81d56eaa --- /dev/null +++ b/assets/arc-0047/.gitignore @@ -0,0 +1,169 @@ +# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp +.cache + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* diff --git a/assets/arc-0047/bun.lockb b/assets/arc-0047/bun.lockb new file mode 100755 index 000000000..7455fad91 Binary files /dev/null and b/assets/arc-0047/bun.lockb differ diff --git a/assets/arc-0047/dapp.ts b/assets/arc-0047/dapp.ts new file mode 100644 index 000000000..f080cdd5d --- /dev/null +++ b/assets/arc-0047/dapp.ts @@ -0,0 +1,49 @@ +import algosdk from 'algosdk' +import { readFileSync } from 'fs' +import {canonicalize} from 'json-canonicalize' +import { formatJsonRpcRequest } from "@json-rpc-tools/utils"; +import { sha256 } from 'js-sha256'; + +const algodClient = new algosdk.Algodv2('a'.repeat(64), 'http://localhost', 4001) + +const teal = readFileSync('./lsig.teal').toString() + +const arc47 = { + name: "25000 block payment", + description: "Allows a payment to be made every 25000 blocks of a specific amount to a specific address", + program: btoa(teal), + variables: [ + { + name: "Payment Amount", + variable: "TMPL_AMOUNT", + type: "uint64", + description: "Amount of the payment transaction in microAlgos" + }, + { + name: "Payment Receiver", + variable: "TMPL_RECEIVER", + type: "address", + description: "Address to which the payment transaction is sent" + } + ], +} + +const values: Record = { + TMPL_AMOUNT: 1000000, + TMPL_RECEIVER: 'Y76M3MSY6DKBRHBL7C3NNDXGS5IIMQVQVUAB6MP4XEMMGVF2QWNPL226CA' +} + +let finalTeal = teal + +for (const variable in values) { + finalTeal = finalTeal.replaceAll(variable, values[variable].toString()) +} + +const result = await algodClient.compile(finalTeal).do() + +const requestParams = [canonicalize(arc47), JSON.stringify(values), result.hash] + +const walletConnectRequest = formatJsonRpcRequest('algo_signTemplatedLsig', requestParams); + +console.log(`Request Params: ${console.log(requestParams)}`) +console.log(`ARC47 SHA256: ${sha256(canonicalize(arc47))}`) \ No newline at end of file diff --git a/assets/arc-0047/lsig.teal b/assets/arc-0047/lsig.teal new file mode 100644 index 000000000..347848074 --- /dev/null +++ b/assets/arc-0047/lsig.teal @@ -0,0 +1,53 @@ +#pragma version 9 + +// Verify this is a payment +txn TypeEnum +int pay +== + +// Verify this is not rekeying the sender address +txn RekeyTo +global ZeroAddress +== +assert + +// Verify the sender's account is not being closed +txn CloseRemainderTo +global ZeroAddress +== +assert + +// Verify the receiver is equal to the templated receiver address +txn Receiver +addr TMPL_RECEIVER +== +assert + +// Verify the amount is equal to the templated amount +txn Amount +int TMPL_AMOUNT +== +assert + +// Verify the current round is within 500 rounds of a product of 25_000 +global Round +int 25_000 +% +store 0 + +load 0 +int 500 +<= + +load 0 +int 24_500 +>= + +|| +assert + +// Verify lease +txn Lease +byte "scheduled 25_000 payment" +sha256 +== diff --git a/assets/arc-0047/package.json b/assets/arc-0047/package.json new file mode 100644 index 000000000..90d11f830 --- /dev/null +++ b/assets/arc-0047/package.json @@ -0,0 +1,18 @@ +{ + "name": "lsig-template-example", + "module": "index.ts", + "type": "module", + "devDependencies": { + "bun-types": "latest" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "dependencies": { + "@json-rpc-tools/utils": "^1.7.6", + "algosdk": "^2.5.0", + "js-sha256": "^0.10.1", + "json-canonicalize": "^1.0.6", + "tslib": "^2.6.2" + } +} \ No newline at end of file diff --git a/assets/arc-0047/tsconfig.json b/assets/arc-0047/tsconfig.json new file mode 100644 index 000000000..1449bc3d9 --- /dev/null +++ b/assets/arc-0047/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "module": "esnext", + "target": "esnext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "allowImportingTsExtensions": true, + "noEmit": true, + "composite": true, + "strict": true, + "downlevelIteration": true, + "skipLibCheck": true, + "jsx": "preserve", + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "types": [ + "bun-types" // add Bun global + ] + } +} diff --git a/assets/arc-0047/wallet.ts b/assets/arc-0047/wallet.ts new file mode 100644 index 000000000..9d9664183 --- /dev/null +++ b/assets/arc-0047/wallet.ts @@ -0,0 +1,44 @@ +import algosdk from 'algosdk' +import { sha256 } from 'js-sha256' +import { canonicalize } from 'json-canonicalize' + +const algodClient = new algosdk.Algodv2('a'.repeat(64), 'http://localhost', 4001) + +const allowList: string[] = ['032c6b017bdad49f54d41170fef9c13acdb8e5ff9a76fcaf0cfbecb6b3fdb5d0'] + +const mockRequest = [ "{\"description\":\"Allows a payment to be made every 25000 blocks of a specific amount to a specific address\",\"name\":\"25000 block payment\",\"program\":\"I3ByYWdtYSB2ZXJzaW9uIDkKCi8vIFZlcmlmeSB0aGlzIGlzIGEgcGF5bWVudAp0eG4gVHlwZUVudW0KaW50IHBheQo9PQoKLy8gVmVyaWZ5IHRoaXMgaXMgbm90IHJla2V5aW5nIHRoZSBzZW5kZXIgYWRkcmVzcwp0eG4gUmVrZXlUbwpnbG9iYWwgWmVyb0FkZHJlc3MKPT0KYXNzZXJ0CgovLyBWZXJpZnkgdGhlIHNlbmRlcidzIGFjY291bnQgaXMgbm90IGJlaW5nIGNsb3NlZAp0eG4gQ2xvc2VSZW1haW5kZXJUbwpnbG9iYWwgWmVyb0FkZHJlc3MKPT0KYXNzZXJ0CgovLyBWZXJpZnkgdGhlIHJlY2VpdmVyIGlzIGVxdWFsIHRvIHRoZSB0ZW1wbGF0ZWQgcmVjZWl2ZXIgYWRkcmVzcwp0eG4gUmVjZWl2ZXIKYWRkciBUTVBMX1JFQ0VJVkVSCj09CmFzc2VydAoKLy8gVmVyaWZ5IHRoZSBhbW91bnQgaXMgZXF1YWwgdG8gdGhlIHRlbXBsYXRlZCBhbW91bnQKdHhuIEFtb3VudAppbnQgVE1QTF9BTU9VTlQKPT0KYXNzZXJ0CgovLyBWZXJpZnkgdGhlIGN1cnJlbnQgcm91bmQgaXMgd2l0aGluIDUwMCByb3VuZHMgb2YgYSBwcm9kdWN0IG9mIDI1XzAwMApnbG9iYWwgUm91bmQKaW50IDI1XzAwMAolCnN0b3JlIDAKCmxvYWQgMAppbnQgNTAwCjw9Cgpsb2FkIDAKaW50IDI0XzUwMAo+PQoKfHwKYXNzZXJ0CgovLyBWZXJpZnkgbGVhc2UgCnR4biBMZWFzZQpieXRlICJzY2hlZHVsZWQgMjVfMDAwIHBheW1lbnQiCnNoYTI1Ngo9PQo=\",\"variables\":[{\"description\":\"Amount of the payment transaction in microAlgos\",\"name\":\"Payment Amount\",\"type\":\"uint64\",\"variable\":\"TMPL_AMOUNT\"},{\"description\":\"Address to which the payment transaction is sent\",\"name\":\"Payment Receiver\",\"type\":\"address\",\"variable\":\"TMPL_RECEIVER\"}]}", +"{\"TMPL_AMOUNT\":1000000,\"TMPL_RECEIVER\":\"Y76M3MSY6DKBRHBL7C3NNDXGS5IIMQVQVUAB6MP4XEMMGVF2QWNPL226CA\"}", +"6INR7PDVBEVPFXMYOWG2J7KLGMQUWKB7CFX3KW2ERW4E42NW5R7WVB4R3A" ] + +async function processTemplatedLsig(requestParams: string[], signer: (lsig: algosdk.LogicSig) => Promise): Promise { + const arc47 = JSON.parse(requestParams[0]) + const values = JSON.parse(requestParams[1]) + const hash = requestParams[2] + + // allowList is not a required feature of ARC47, but it allows wallets to verify the lsig template before signing + if (!allowList.includes(sha256(canonicalize(arc47)))) throw Error('Templated Lsig not in allow list') + + // base64 decode the program + let finalTeal = atob(arc47.program) + + // substitute the variables + for (const variable in values) { + finalTeal = finalTeal.replaceAll(variable, values[variable].toString()) + } + + // use algod to compile the TEAL after substituting the variables + const compileResponse = await algodClient.compile(finalTeal).do() + + // verify the compiled hash is the same as the given hash in the request + if (compileResponse.hash !== hash) throw Error(`Compiled hash (${compileResponse.hash}) does not match expected hash (${hash})`) + + // create a LogicSig object from the compiled program + const lsig = new algosdk.LogicSig(Buffer.from(compileResponse.result, 'base64')) + + // signer function is expected to return the signature of the lsig + return signer(lsig) +} + +const signature = await processTemplatedLsig(mockRequest, async (lsig) => lsig.signProgram((algosdk.generateAccount()).sk)) + +console.log(signature)