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)