diff --git a/dynamic_records/gold_token/.env b/dynamic_records/gold_token/.env new file mode 100644 index 0000000..ab204ee --- /dev/null +++ b/dynamic_records/gold_token/.env @@ -0,0 +1,3 @@ +NETWORK=testnet +PRIVATE_KEY=APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH +ENDPOINT=http://localhost:3030 diff --git a/dynamic_records/gold_token/build/abi.json b/dynamic_records/gold_token/build/abi.json new file mode 100644 index 0000000..ce6f540 --- /dev/null +++ b/dynamic_records/gold_token/build/abi.json @@ -0,0 +1,180 @@ +{ + "program": "gold_token.aleo", + "structs": [], + "records": [ + { + "path": [ + "Token" + ], + "fields": [ + { + "name": "owner", + "ty": { + "Primitive": "Address" + }, + "mode": "None" + }, + { + "name": "amount", + "ty": { + "Primitive": { + "UInt": "U64" + } + }, + "mode": "None" + }, + { + "name": "purity", + "ty": { + "Primitive": { + "UInt": "U64" + } + }, + "mode": "None" + } + ] + } + ], + "mappings": [], + "storage_variables": [], + "functions": [ + { + "name": "mint", + "is_final": false, + "inputs": [ + { + "name": "owner", + "ty": { + "Plaintext": { + "Primitive": "Address" + } + }, + "mode": "None" + }, + { + "name": "amount", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U64" + } + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Record": { + "path": [ + "Token" + ], + "program": "gold_token.aleo" + } + }, + "mode": "None" + } + ] + }, + { + "name": "mint_custom", + "is_final": false, + "inputs": [ + { + "name": "owner", + "ty": { + "Plaintext": { + "Primitive": "Address" + } + }, + "mode": "None" + }, + { + "name": "amount", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U64" + } + } + }, + "mode": "None" + }, + { + "name": "purity", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U64" + } + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Record": { + "path": [ + "Token" + ], + "program": "gold_token.aleo" + } + }, + "mode": "None" + } + ] + }, + { + "name": "transfer", + "is_final": false, + "inputs": [ + { + "name": "token", + "ty": "DynamicRecord", + "mode": "None" + }, + { + "name": "to", + "ty": { + "Plaintext": { + "Primitive": "Address" + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": "DynamicRecord", + "mode": "None" + } + ] + }, + { + "name": "balance_of", + "is_final": false, + "inputs": [ + { + "name": "token", + "ty": "DynamicRecord", + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U64" + } + } + }, + "mode": "None" + } + ] + } + ] +} \ No newline at end of file diff --git a/dynamic_records/gold_token/build/main.aleo b/dynamic_records/gold_token/build/main.aleo new file mode 100644 index 0000000..a979a62 --- /dev/null +++ b/dynamic_records/gold_token/build/main.aleo @@ -0,0 +1,40 @@ +program gold_token.aleo; + +record Token: + owner as address.private; + amount as u64.private; + purity as u64.private; + +function mint: + input r0 as address.private; + input r1 as u64.private; + cast r0 r1 24u64 into r2 as Token.record; + output r2 as Token.record; + +function mint_custom: + input r0 as address.private; + input r1 as u64.private; + input r2 as u64.private; + gte r2 1u64 into r3; + lte r2 24u64 into r4; + and r3 r4 into r5; + assert.eq r5 true; + cast r0 r1 r2 into r6 as Token.record; + output r6 as Token.record; + +function transfer: + input r0 as dynamic.record; + input r1 as address.private; + get.record.dynamic r0.amount into r2 as u64; + get.record.dynamic r0.purity into r3 as u64; + cast r1 r2 r3 into r4 as Token.record; + cast r4 into r5 as dynamic.record; + output r5 as dynamic.record; + +function balance_of: + input r0 as dynamic.record; + get.record.dynamic r0.amount into r1 as u64; + output r1 as u64.private; + +constructor: + assert.eq edition 0u16; diff --git a/dynamic_records/gold_token/build/program.json b/dynamic_records/gold_token/build/program.json new file mode 100644 index 0000000..dec2f23 --- /dev/null +++ b/dynamic_records/gold_token/build/program.json @@ -0,0 +1,9 @@ +{ + "program": "gold_token.aleo", + "version": "0.1.0", + "description": "", + "license": "", + "leo": "3.5.0", + "dependencies": null, + "dev_dependencies": null +} diff --git a/dynamic_records/gold_token/program.json b/dynamic_records/gold_token/program.json new file mode 100644 index 0000000..b6ec7e9 --- /dev/null +++ b/dynamic_records/gold_token/program.json @@ -0,0 +1,6 @@ +{ + "program": "gold_token.aleo", + "version": "0.1.0", + "description": "Gold token implementing the TokenStandard interface. Carries a purity field beyond the interface minimum.", + "license": "MIT" +} diff --git a/dynamic_records/gold_token/src/main.leo b/dynamic_records/gold_token/src/main.leo new file mode 100644 index 0000000..c43a79d --- /dev/null +++ b/dynamic_records/gold_token/src/main.leo @@ -0,0 +1,61 @@ +// gold_token.aleo — private gold token implementing the TokenStandard interface. +// +// The Token record carries an extra `purity` field (karat value, 1–24) beyond +// the minimum fields required by TokenStandard. When gold_token.aleo is +// called via dynamic dispatch, the caller receives the token as `dyn record` +// and never needs to know about `purity` at compile time. +interface TokenStandard { + record Token { + owner: address, + amount: u64, + .. // implementors may add extra fields beyond these + } + fn mint(owner: address, amount: u64) -> Token; + fn transfer(token: dyn record, to: address) -> dyn record; + fn balance_of(token: dyn record) -> u64; +} + +program gold_token.aleo : TokenStandard { + // Token carries purity (karat value) alongside the standard fields. + // This extra field is invisible to the router at compile time but remains + // accessible via `dyn record` field access inside this program. + record Token { + owner: address, + amount: u64, // milligrams + purity: u64, // karat value: 1 (lowest) to 24 (pure gold) + } + + // Creates a Token at maximum purity (24-karat). + // This satisfies the interface's `mint` requirement with a sensible default. + fn mint(owner: address, amount: u64) -> Token { + return Token { owner, amount, purity: 24u64 }; + } + + // Creates a Token with a custom purity level. + // This is a program-specific function — not required by the interface. + fn mint_custom(owner: address, amount: u64, purity: u64) -> Token { + assert(purity >= 1u64 && purity <= 24u64); + return Token { owner, amount, purity }; + } + + // Transfers the token to a new owner. + // + // `token` arrives as `dyn record` because dynamic calls always erase the + // concrete type. We read the fields we care about — including the + // program-specific `purity` field — rebuild the record under the new + // owner, then cast back to `dyn record` for the return value. + fn transfer(token: dyn record, to: address) -> dyn record { + let amount: u64 = token.amount; + let purity: u64 = token.purity; // dyn record field access — fails at runtime if missing + let new_token: Token = Token { owner: to, amount, purity }; + return new_token as dyn record; // explicit cast: static record → dyn record + } + + // Returns the token's amount without needing to know its full structure. + fn balance_of(token: dyn record) -> u64 { + return token.amount; + } + + @noupgrade + constructor() {} +} diff --git a/dynamic_records/run.sh b/dynamic_records/run.sh new file mode 100755 index 0000000..aaa0fdc --- /dev/null +++ b/dynamic_records/run.sh @@ -0,0 +1,107 @@ +#!/bin/bash +# Demonstrates dynamic records and dynamic dispatch in Leo. +# Run from the dynamic_records/ directory. +# +# Prerequisites: +# leo devnode must be running in a separate terminal: +# leo devnode start --private-key APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH + +set -euo pipefail + +LEO=${LEO:-leo} + +OWNER="aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px" +PRIVATE_KEY="APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH" +COMMON_OPTS=(-y --disable-update-check --broadcast + --network testnet + --endpoint "http://localhost:${LEO_DEVNODE_PORT:-3030}" + --private-key "$PRIVATE_KEY" + --consensus-heights "0,1,2,3,4,5,6,7,8,9,10,11,12,13") + +# ─── Part 1: Local execution (no devnode required) ──────────────────────────── + +echo "" +echo "═══════════════════════════════════════════════════════" +echo " PART 1: Local execution (no devnode needed)" +echo "═══════════════════════════════════════════════════════" + +echo "" +echo "── gold_token.aleo ──────────────────────────────────────" +echo " The Token record carries a purity field beyond the interface minimum." +cd gold_token +echo " 1 000 mg at 24-karat (pure gold):" +$LEO run mint_custom "$OWNER" 1000u64 24u64 +echo "" +echo " 500 mg at 18-karat:" +$LEO run mint_custom "$OWNER" 500u64 18u64 +cd .. + +echo "" +echo "── silver_token.aleo ────────────────────────────────────" +echo " The Token record carries a grade field — a different extra field" +echo " from GoldToken, yet both satisfy the same TokenStandard interface." +cd silver_token +echo " 2 000 mg at sterling grade (3):" +$LEO run mint_custom "$OWNER" 2000u64 3u64 +echo "" +echo " 800 mg at industrial grade (0):" +$LEO run mint_custom "$OWNER" 800u64 0u64 +cd .. + +# ─── Part 2: Deployment and dynamic dispatch ────────────────────────────────── + +echo "" +echo "═══════════════════════════════════════════════════════" +echo " PART 2: Deployment and dynamic dispatch" +echo " (requires leo devnode)" +echo "═══════════════════════════════════════════════════════" + +echo "" +echo "── Step 1: Deploy gold_token.aleo ───────────────────────" +echo " (defines TokenStandard interface + gold implementation)" +cd gold_token +$LEO deploy "${COMMON_OPTS[@]}" +cd .. + +echo "" +echo "── Step 2: Deploy silver_token.aleo ─────────────────────" +echo " (silver implementation of TokenStandard)" +cd silver_token +$LEO deploy "${COMMON_OPTS[@]}" +cd .. + +echo "" +echo "── Step 3: Deploy token_router.aleo ─────────────────────" +echo " (dynamic dispatch hub — no token logic of its own)" +cd token_router +$LEO deploy "${COMMON_OPTS[@]}" + +echo "" +echo "── Step 4: demo_transfer — mint then route in one dynamic call ──" +echo " The router dispatches two calls in sequence without ever knowing" +echo " the concrete record type (GoldToken has 'purity'; SilverToken has 'grade')." + +echo "" +echo " Minting 1 000 mg gold (purity=24) then routing to owner:" +$LEO execute token_router.aleo/demo_transfer "'gold_token'" "$OWNER" 1000u64 "$OWNER" "${COMMON_OPTS[@]}" + +echo "" +echo " Minting 2 000 mg silver (grade=3) then routing to owner:" +$LEO execute token_router.aleo/demo_transfer "'silver_token'" "$OWNER" 2000u64 "$OWNER" "${COMMON_OPTS[@]}" + +echo "" +echo "── Step 5: route_transfer and read_balance (manual) ────────" +echo " These functions accept a dyn record from a previous mint." +echo " Capture a record ciphertext from step 4 above, then run:" +echo "" +echo " leo execute token_router.aleo/read_balance \\" +echo " \"'gold_token'\" ${COMMON_OPTS[*]}" +echo "" +echo " leo execute token_router.aleo/route_transfer \\" +echo " \"'gold_token'\" \"$OWNER\" ${COMMON_OPTS[*]}" + +cd .. + +echo "" +echo "Done! token_router.aleo routed mints and transfers to GoldToken and" +echo "SilverToken without knowing either record's concrete field layout." diff --git a/dynamic_records/silver_token/.env b/dynamic_records/silver_token/.env new file mode 100644 index 0000000..ab204ee --- /dev/null +++ b/dynamic_records/silver_token/.env @@ -0,0 +1,3 @@ +NETWORK=testnet +PRIVATE_KEY=APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH +ENDPOINT=http://localhost:3030 diff --git a/dynamic_records/silver_token/build/abi.json b/dynamic_records/silver_token/build/abi.json new file mode 100644 index 0000000..1543046 --- /dev/null +++ b/dynamic_records/silver_token/build/abi.json @@ -0,0 +1,180 @@ +{ + "program": "silver_token.aleo", + "structs": [], + "records": [ + { + "path": [ + "Token" + ], + "fields": [ + { + "name": "owner", + "ty": { + "Primitive": "Address" + }, + "mode": "None" + }, + { + "name": "amount", + "ty": { + "Primitive": { + "UInt": "U64" + } + }, + "mode": "None" + }, + { + "name": "grade", + "ty": { + "Primitive": { + "UInt": "U64" + } + }, + "mode": "None" + } + ] + } + ], + "mappings": [], + "storage_variables": [], + "functions": [ + { + "name": "mint", + "is_final": false, + "inputs": [ + { + "name": "owner", + "ty": { + "Plaintext": { + "Primitive": "Address" + } + }, + "mode": "None" + }, + { + "name": "amount", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U64" + } + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Record": { + "path": [ + "Token" + ], + "program": "silver_token.aleo" + } + }, + "mode": "None" + } + ] + }, + { + "name": "mint_custom", + "is_final": false, + "inputs": [ + { + "name": "owner", + "ty": { + "Plaintext": { + "Primitive": "Address" + } + }, + "mode": "None" + }, + { + "name": "amount", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U64" + } + } + }, + "mode": "None" + }, + { + "name": "grade", + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U64" + } + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Record": { + "path": [ + "Token" + ], + "program": "silver_token.aleo" + } + }, + "mode": "None" + } + ] + }, + { + "name": "transfer", + "is_final": false, + "inputs": [ + { + "name": "token", + "ty": "DynamicRecord", + "mode": "None" + }, + { + "name": "to", + "ty": { + "Plaintext": { + "Primitive": "Address" + } + }, + "mode": "None" + } + ], + "outputs": [ + { + "ty": "DynamicRecord", + "mode": "None" + } + ] + }, + { + "name": "balance_of", + "is_final": false, + "inputs": [ + { + "name": "token", + "ty": "DynamicRecord", + "mode": "None" + } + ], + "outputs": [ + { + "ty": { + "Plaintext": { + "Primitive": { + "UInt": "U64" + } + } + }, + "mode": "None" + } + ] + } + ] +} \ No newline at end of file diff --git a/dynamic_records/silver_token/build/main.aleo b/dynamic_records/silver_token/build/main.aleo new file mode 100644 index 0000000..4409237 --- /dev/null +++ b/dynamic_records/silver_token/build/main.aleo @@ -0,0 +1,38 @@ +program silver_token.aleo; + +record Token: + owner as address.private; + amount as u64.private; + grade as u64.private; + +function mint: + input r0 as address.private; + input r1 as u64.private; + cast r0 r1 3u64 into r2 as Token.record; + output r2 as Token.record; + +function mint_custom: + input r0 as address.private; + input r1 as u64.private; + input r2 as u64.private; + lte r2 3u64 into r3; + assert.eq r3 true; + cast r0 r1 r2 into r4 as Token.record; + output r4 as Token.record; + +function transfer: + input r0 as dynamic.record; + input r1 as address.private; + get.record.dynamic r0.amount into r2 as u64; + get.record.dynamic r0.grade into r3 as u64; + cast r1 r2 r3 into r4 as Token.record; + cast r4 into r5 as dynamic.record; + output r5 as dynamic.record; + +function balance_of: + input r0 as dynamic.record; + get.record.dynamic r0.amount into r1 as u64; + output r1 as u64.private; + +constructor: + assert.eq edition 0u16; diff --git a/dynamic_records/silver_token/build/program.json b/dynamic_records/silver_token/build/program.json new file mode 100644 index 0000000..2c266c5 --- /dev/null +++ b/dynamic_records/silver_token/build/program.json @@ -0,0 +1,9 @@ +{ + "program": "silver_token.aleo", + "version": "0.1.0", + "description": "", + "license": "", + "leo": "3.5.0", + "dependencies": null, + "dev_dependencies": null +} diff --git a/dynamic_records/silver_token/program.json b/dynamic_records/silver_token/program.json new file mode 100644 index 0000000..280e9fe --- /dev/null +++ b/dynamic_records/silver_token/program.json @@ -0,0 +1,6 @@ +{ + "program": "silver_token.aleo", + "version": "0.1.0", + "description": "Silver token implementing the TokenStandard interface. Carries a grade field beyond the interface minimum.", + "license": "MIT" +} diff --git a/dynamic_records/silver_token/src/main.leo b/dynamic_records/silver_token/src/main.leo new file mode 100644 index 0000000..83d9c40 --- /dev/null +++ b/dynamic_records/silver_token/src/main.leo @@ -0,0 +1,53 @@ +// silver_token.aleo — private silver token implementing the TokenStandard interface. +// +// Its Token record adds a `grade` field (quality tier: 0 = industrial, 3 = sterling) +// instead of a purity field. Despite a different extra field from GoldToken, +// silver_token.aleo is fully interchangeable through TokenStandard: the router +// treats both uniformly without knowing either concrete record structure. +interface TokenStandard { + record Token { + owner: address, + amount: u64, + .. + } + fn mint(owner: address, amount: u64) -> Token; + fn transfer(token: dyn record, to: address) -> dyn record; + fn balance_of(token: dyn record) -> u64; +} + +program silver_token.aleo : TokenStandard { + // Token carries a `grade` field instead of purity — a different extra field + // than GoldToken, demonstrating that `..` allows each implementor to diverge. + record Token { + owner: address, + amount: u64, // milligrams + grade: u64, // quality tier: 0 (industrial) to 3 (sterling) + } + + // Creates a Token at sterling grade (highest quality). + fn mint(owner: address, amount: u64) -> Token { + return Token { owner, amount, grade: 3u64 }; + } + + // Creates a Token with a custom grade. + fn mint_custom(owner: address, amount: u64, grade: u64) -> Token { + assert(grade <= 3u64); + return Token { owner, amount, grade }; + } + + // Transfers the token to a new owner. + // Reads `grade` from the incoming `dyn record` and carries it to the new token. + fn transfer(token: dyn record, to: address) -> dyn record { + let amount: u64 = token.amount; + let grade: u64 = token.grade; + let new_token: Token = Token { owner: to, amount, grade }; + return new_token as dyn record; + } + + fn balance_of(token: dyn record) -> u64 { + return token.amount; + } + + @noupgrade + constructor() {} +} diff --git a/dynamic_records/token_router/.env b/dynamic_records/token_router/.env new file mode 100644 index 0000000..ab204ee --- /dev/null +++ b/dynamic_records/token_router/.env @@ -0,0 +1,3 @@ +NETWORK=testnet +PRIVATE_KEY=APrivateKey1zkp8CZNn3yeCseEtxuVPbDCwSyhGW6yZKUYKfgXmcpoGPWH +ENDPOINT=http://localhost:3030 diff --git a/dynamic_records/token_router/program.json b/dynamic_records/token_router/program.json new file mode 100644 index 0000000..bb684d0 --- /dev/null +++ b/dynamic_records/token_router/program.json @@ -0,0 +1,10 @@ +{ + "program": "token_router.aleo", + "version": "0.1.0", + "description": "Generic token router. Routes transfers and reads balances for any TokenStandard-compliant program via dynamic dispatch and dyn record.", + "license": "MIT", + "dependencies": [ + { "name": "gold_token.aleo", "location": "network", "path": null }, + { "name": "silver_token.aleo", "location": "network", "path": null } + ] +} diff --git a/dynamic_records/token_router/src/main.leo b/dynamic_records/token_router/src/main.leo new file mode 100644 index 0000000..604e07b --- /dev/null +++ b/dynamic_records/token_router/src/main.leo @@ -0,0 +1,93 @@ +// token_router.aleo routes token operations to any TokenStandard-compliant program. +// +// The router never knows the concrete field layout of GoldToken or SilverToken. +// All record I/O uses `dyn record`, letting the router handle any compliant token — +// including programs deployed after token_router.aleo itself. +// +// Dynamic call syntax used here: +// TokenStandard@(token_program)/transfer(token, to) +// ├─ TokenStandard → interface declared in this file +// ├─ @(token_program) → runtime-resolved target (an `identifier` value) +// └─ /transfer → function to invoke on that program +interface TokenStandard { + record Token { + owner: address, + amount: u64, + .. + } + fn mint(owner: address, amount: u64) -> Token; + fn transfer(token: dyn record, to: address) -> dyn record; + fn balance_of(token: dyn record) -> u64; +} + +program token_router.aleo { + // Routes a transfer to any TokenStandard-compliant program at runtime. + // + // Case A (dyn record in, dyn record out): the caller already holds a `dyn record` + // and passes it directly. No cast is needed. The return type is always + // `dyn record` regardless of what the interface signature declares. + fn route_transfer( + token_program: identifier, + token: dyn record, + to: address, + ) -> dyn record { + return TokenStandard@(token_program)/transfer(token, to); + } + + // Reads the balance from any token without knowing its concrete type. + // + // The `dyn record` field access (`.amount`) happens inside `balance_of` on + // the target program — the router itself never touches record fields here. + fn read_balance(token_program: identifier, token: dyn record) -> u64 { + return TokenStandard@(token_program)/balance_of(token); + } + + // Mints a token and immediately routes it to a recipient — all in one call. + // + // `mint` returns a concrete Token on the target program; through dynamic + // dispatch the router receives it as `dyn record` and forwards it directly + // to `transfer`. Two dynamic calls, zero compile-time knowledge of the + // underlying record type. + fn demo_transfer( + token_program: identifier, + owner: address, + amount: u64, + to: address, + ) -> dyn record { + let token: dyn record = TokenStandard@(token_program)/mint(owner, amount); + return TokenStandard@(token_program)/transfer(token, to); + } + + // Compares gold and silver balances using identifier literals. + // + // The targets 'gold_token' and 'silver_token' are pinned at compile time + // (and verified at deploy time) while still going through the dynamic- + // dispatch path at the AVM level. Returns true if the gold token holds + // a larger amount than the silver token. + fn gold_beats_silver( + gold_tok: dyn record, + silver_tok: dyn record, + ) -> bool { + let gold_bal: u64 = TokenStandard@('gold_token')/balance_of(gold_tok); + let silver_bal: u64 = TokenStandard@('silver_token')/balance_of(silver_tok); + return gold_bal > silver_bal; + } + + // Compares balances across any two token programs resolved at runtime. + // + // `prog_a` and `prog_b` can be the same program or different ones — + // the router treats them uniformly. + fn has_more( + prog_a: identifier, + tok_a: dyn record, + prog_b: identifier, + tok_b: dyn record, + ) -> bool { + let bal_a: u64 = TokenStandard@(prog_a)/balance_of(tok_a); + let bal_b: u64 = TokenStandard@(prog_b)/balance_of(tok_b); + return bal_a > bal_b; + } + + @noupgrade + constructor() {} +}