chore: initial commit for v0.0.1
DChain single-node blockchain + React Native messenger client. Core: - PBFT consensus with multi-sig validator admission + equivocation slashing - BadgerDB + schema migration scaffold (CurrentSchemaVersion=0) - libp2p gossipsub (tx/v1, blocks/v1, relay/v1, version/v1) - Native Go contracts (username_registry) alongside WASM (wazero) - WebSocket gateway with topic-based fanout + Ed25519-nonce auth - Relay mailbox with NaCl envelope encryption (X25519 + Ed25519) - Prometheus /metrics, per-IP rate limit, body-size cap Deployment: - Single-node compose (deploy/single/) with Caddy TLS + optional Prometheus - 3-node dev compose (docker-compose.yml) with mocked internet topology - 3-validator prod compose (deploy/prod/) for federation - Auto-update from Gitea via /api/update-check + systemd timer - Build-time version injection (ldflags → node --version) - UI / Swagger toggle flags (DCHAIN_DISABLE_UI, DCHAIN_DISABLE_SWAGGER) Client (client-app/): - Expo / React Native / NativeWind - E2E NaCl encryption, typing indicator, contact requests - Auto-discovery of canonical contracts, chain_id aware, WS reconnect on node switch Documentation: - README.md, CHANGELOG.md, CONTEXT.md - deploy/single/README.md with 6 operator scenarios - deploy/UPDATE_STRATEGY.md with 4-layer forward-compat design - docs/contracts/*.md per contract
This commit is contained in:
BIN
contracts/auction/auction.wasm
Normal file
BIN
contracts/auction/auction.wasm
Normal file
Binary file not shown.
45
contracts/auction/auction_abi.json
Normal file
45
contracts/auction/auction_abi.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"contract": "auction",
|
||||
"version": "1.0.0",
|
||||
"description": "English auction with on-chain token escrow. Bids are locked in the contract treasury. When an auction settles, the seller receives the top bid; outbid funds are automatically refunded.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "create",
|
||||
"description": "Create a new auction. Logs 'created: <auction_id>'. auction_id = block_height:seq (seq auto-incremented).",
|
||||
"args": [
|
||||
{"name": "title", "type": "string", "description": "Human-readable item title (max 128 chars)"},
|
||||
{"name": "min_bid", "type": "uint64", "description": "Minimum opening bid in µT"},
|
||||
{"name": "duration", "type": "uint64", "description": "Duration in blocks"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "bid",
|
||||
"description": "Place a bid on an open auction. Caller transfers bid amount to the contract treasury; previous top bidder is refunded. Logs 'bid: <auction_id>'.",
|
||||
"args": [
|
||||
{"name": "auction_id", "type": "string", "description": "Auction ID returned by create"},
|
||||
{"name": "amount", "type": "uint64", "description": "Bid amount in µT (must exceed current top bid)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "settle",
|
||||
"description": "Settle an ended auction. Transfers winning bid to seller. Anyone may call after the end block. Logs 'settled: <auction_id>'.",
|
||||
"args": [
|
||||
{"name": "auction_id", "type": "string", "description": "Auction ID to settle"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "cancel",
|
||||
"description": "Cancel an auction before any bids. Only the seller may cancel. Logs 'cancelled: <auction_id>'.",
|
||||
"args": [
|
||||
{"name": "auction_id", "type": "string", "description": "Auction ID to cancel"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"description": "Query auction info. Logs 'seller: ...', 'title: ...', 'top_bid: ...', 'end_block: ...', 'status: open/settled/cancelled'.",
|
||||
"args": [
|
||||
{"name": "auction_id", "type": "string", "description": "Auction ID"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
784
contracts/auction/gen/main.go
Normal file
784
contracts/auction/gen/main.go
Normal file
@@ -0,0 +1,784 @@
|
||||
// gen generates contracts/auction/auction.wasm
|
||||
// Run from repo root: go run ./contracts/auction/gen/
|
||||
//
|
||||
// Methods: create, bid, settle, cancel, info
|
||||
//
|
||||
// State keys (per auction_id, single-char suffix):
|
||||
// "a:<id>:s" → seller address
|
||||
// "a:<id>:t" → title
|
||||
// "a:<id>:m" → min_bid (8-byte big-endian u64 via put_u64/get_u64)
|
||||
// "a:<id>:e" → end_block (8-byte big-endian u64)
|
||||
// "a:<id>:b" → top_bidder address (empty string = no bid)
|
||||
// "a:<id>:v" → top_bid amount (8-byte big-endian u64)
|
||||
// "a:<id>:x" → status byte ('o'=open, 's'=settled, 'c'=cancelled)
|
||||
//
|
||||
// The contract treasury holds in-flight bid escrow.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ── LEB128 & builders ────────────────────────────────────────────────────────
|
||||
|
||||
func u(v uint64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
if v != 0 {
|
||||
bt |= 0x80
|
||||
}
|
||||
b = append(b, bt)
|
||||
if v == 0 {
|
||||
return b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func s(v int64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
sign := (bt & 0x40) != 0
|
||||
if (v == 0 && !sign) || (v == -1 && sign) {
|
||||
return append(b, bt)
|
||||
}
|
||||
b = append(b, bt|0x80)
|
||||
}
|
||||
}
|
||||
|
||||
func cat(slices ...[]byte) []byte {
|
||||
var out []byte
|
||||
for _, sl := range slices {
|
||||
out = append(out, sl...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func wstr(str string) []byte { return cat(u(uint64(len(str))), []byte(str)) }
|
||||
|
||||
func section(id byte, content []byte) []byte {
|
||||
return cat([]byte{id}, u(uint64(len(content))), content)
|
||||
}
|
||||
|
||||
func vec(items ...[]byte) []byte {
|
||||
out := u(uint64(len(items)))
|
||||
for _, it := range items {
|
||||
out = append(out, it...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func functype(params, results []byte) []byte {
|
||||
return cat([]byte{0x60}, u(uint64(len(params))), params, u(uint64(len(results))), results)
|
||||
}
|
||||
|
||||
func importFunc(mod, name string, typeIdx uint32) []byte {
|
||||
return cat(wstr(mod), wstr(name), []byte{0x00}, u(uint64(typeIdx)))
|
||||
}
|
||||
|
||||
func exportEntry(name string, kind byte, idx uint32) []byte {
|
||||
return cat(wstr(name), []byte{kind}, u(uint64(idx)))
|
||||
}
|
||||
|
||||
func dataSegment(offset int32, data []byte) []byte {
|
||||
return cat(
|
||||
[]byte{0x00},
|
||||
[]byte{0x41}, s(int64(offset)), []byte{0x0B},
|
||||
u(uint64(len(data))), data,
|
||||
)
|
||||
}
|
||||
|
||||
func funcBody(localDecls []byte, instrs ...[]byte) []byte {
|
||||
inner := cat(localDecls)
|
||||
for _, ins := range instrs {
|
||||
inner = append(inner, ins...)
|
||||
}
|
||||
inner = append(inner, 0x0B)
|
||||
return cat(u(uint64(len(inner))), inner)
|
||||
}
|
||||
|
||||
var noLocals = u(0)
|
||||
|
||||
func localDecl(n uint32, typ byte) []byte { return cat(u(uint64(n)), []byte{typ}) }
|
||||
func withLocals(decls ...[]byte) []byte {
|
||||
return cat(u(uint64(len(decls))), cat(decls...))
|
||||
}
|
||||
|
||||
const (
|
||||
tI32 byte = 0x7F
|
||||
tI64 byte = 0x7E
|
||||
)
|
||||
|
||||
func call(fn uint32) []byte { return cat([]byte{0x10}, u(uint64(fn))) }
|
||||
func lget(i uint32) []byte { return cat([]byte{0x20}, u(uint64(i))) }
|
||||
func lset(i uint32) []byte { return cat([]byte{0x21}, u(uint64(i))) }
|
||||
func ic32(v int32) []byte { return cat([]byte{0x41}, s(int64(v))) }
|
||||
func ic64(v int64) []byte { return cat([]byte{0x42}, s(v)) }
|
||||
func block_() []byte { return []byte{0x02, 0x40} }
|
||||
func loop_() []byte { return []byte{0x03, 0x40} }
|
||||
func if_() []byte { return []byte{0x04, 0x40} }
|
||||
func else_() []byte { return []byte{0x05} }
|
||||
func end_() []byte { return []byte{0x0B} }
|
||||
func br_(lbl uint32) []byte { return cat([]byte{0x0C}, u(uint64(lbl))) }
|
||||
func brIf_(lbl uint32) []byte { return cat([]byte{0x0D}, u(uint64(lbl))) }
|
||||
func return_() []byte { return []byte{0x0F} }
|
||||
func drop() []byte { return []byte{0x1A} }
|
||||
func i32Eqz() []byte { return []byte{0x45} }
|
||||
func i32Ne() []byte { return []byte{0x47} }
|
||||
func i32GtU() []byte { return []byte{0x4B} }
|
||||
func i32GeU() []byte { return []byte{0x4F} }
|
||||
func i32Add() []byte { return []byte{0x6A} }
|
||||
func i64Eqz() []byte { return []byte{0x50} }
|
||||
func i64Eq() []byte { return []byte{0x51} }
|
||||
func i64GtU() []byte { return []byte{0x56} }
|
||||
func i64LeU() []byte { return []byte{0x57} }
|
||||
func i64Add() []byte { return []byte{0x7C} }
|
||||
func i32Load8U() []byte { return []byte{0x2D, 0x00, 0x00} }
|
||||
func i32Store8() []byte { return []byte{0x3A, 0x00, 0x00} }
|
||||
|
||||
// ── Memory layout ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// 0x000 64 arg[0]: auction_id
|
||||
// 0x040 128 arg[1]: title
|
||||
// 0x140 128 caller buffer
|
||||
// 0x1C0 64 treasury buffer
|
||||
// 0x200 128 state-read: seller or top_bidder
|
||||
// 0x280 128 secondary state-read: top_bidder (during bid)
|
||||
// 0x300 2 state-read: status byte
|
||||
// 0x310 256 scratch (buildField writes here)
|
||||
//
|
||||
// Constant strings at 0x500+:
|
||||
// 0x500 10 "created: "
|
||||
// 0x50A 5 "bid: "
|
||||
// 0x510 10 "settled: "
|
||||
// 0x51A 11 "cancelled: "
|
||||
// 0x526 14 "unauthorized: "
|
||||
// 0x535 9 "not found: "
|
||||
// 0x540 12 "not open: " (auction not in open state)
|
||||
// 0x54C 14 "bidding open: " (cannot cancel with bids)
|
||||
// 0x55B 12 "still open: " (auction has not ended yet)
|
||||
// 0x568 11 "low bid: " (bid too low)
|
||||
// 0x574 8 "seller: "
|
||||
// 0x57D 7 "title: "
|
||||
// 0x585 10 "top bid: "
|
||||
// 0x590 11 "end block: "
|
||||
// 0x59C 8 "status: "
|
||||
// 0x5A5 5 "open"
|
||||
// 0x5AA 8 "settled"
|
||||
// 0x5B3 10 "cancelled"
|
||||
|
||||
const (
|
||||
offArg0 int32 = 0x000
|
||||
offArg1 int32 = 0x040
|
||||
offCaller int32 = 0x140
|
||||
offTreasury int32 = 0x1C0
|
||||
offRead1 int32 = 0x200 // seller / first state read
|
||||
offRead2 int32 = 0x280 // top_bidder / second state read
|
||||
offReadStat int32 = 0x300 // status byte
|
||||
offScratch int32 = 0x310 // key scratch
|
||||
|
||||
offCreatedPfx int32 = 0x500
|
||||
offBidPfx int32 = 0x50A
|
||||
offSettledPfx int32 = 0x510
|
||||
offCancelledPfx int32 = 0x51A
|
||||
offUnauthPfx int32 = 0x526
|
||||
offNotFoundPfx int32 = 0x535
|
||||
offNotOpenPfx int32 = 0x540
|
||||
offHasBidsPfx int32 = 0x54C
|
||||
offStillOpenPfx int32 = 0x55B
|
||||
offLowBidPfx int32 = 0x568
|
||||
offSellerPfx int32 = 0x574
|
||||
offTitlePfx int32 = 0x57D
|
||||
offTopBidPfx int32 = 0x585
|
||||
offEndBlockPfx int32 = 0x590
|
||||
offStatusPfx int32 = 0x59C
|
||||
offStrOpen int32 = 0x5A5
|
||||
offStrSettled int32 = 0x5AA
|
||||
offStrCancelled int32 = 0x5B3
|
||||
)
|
||||
|
||||
// ── Import / function indices ─────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
fnGetArgStr = 0
|
||||
fnGetArgU64 = 1
|
||||
fnGetCaller = 2
|
||||
fnGetState = 3
|
||||
fnSetState = 4
|
||||
fnLog = 5
|
||||
fnTransfer = 6
|
||||
fnGetBalance = 7
|
||||
fnGetContractTreasury = 8
|
||||
fnGetBlockHeight = 9
|
||||
fnPutU64 = 10
|
||||
fnGetU64 = 11
|
||||
|
||||
fnBytesEqual = 12
|
||||
fnMemcpy = 13
|
||||
fnLogPrefix = 14
|
||||
fnBuildField = 15 // buildField(idOff, idLen, fieldChar i32) → keyLen i32
|
||||
fnCreate = 16
|
||||
fnBid = 17
|
||||
fnSettle = 18
|
||||
fnCancel = 19
|
||||
fnInfo = 20
|
||||
)
|
||||
|
||||
// ── Helper bodies ─────────────────────────────────────────────────────────────
|
||||
|
||||
func bytesEqualBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(2, tI32)),
|
||||
ic32(1), lset(4), ic32(0), lset(3),
|
||||
block_(), loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1),
|
||||
lget(0), lget(3), i32Add(), i32Load8U(),
|
||||
lget(1), lget(3), i32Add(), i32Load8U(),
|
||||
i32Ne(), if_(), ic32(0), lset(4), br_(2), end_(),
|
||||
lget(3), ic32(1), i32Add(), lset(3),
|
||||
br_(0), end_(), end_(),
|
||||
lget(4),
|
||||
)
|
||||
}
|
||||
|
||||
func memcpyBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(1, tI32)),
|
||||
ic32(0), lset(3),
|
||||
block_(), loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1),
|
||||
lget(0), lget(3), i32Add(),
|
||||
lget(1), lget(3), i32Add(), i32Load8U(),
|
||||
i32Store8(),
|
||||
lget(3), ic32(1), i32Add(), lset(3),
|
||||
br_(0), end_(), end_(),
|
||||
)
|
||||
}
|
||||
|
||||
func logPrefixBody() []byte {
|
||||
return funcBody(
|
||||
noLocals,
|
||||
ic32(offScratch), lget(0), lget(1), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), i32Add(), lget(2), lget(3), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), lget(3), i32Add(), call(fnLog),
|
||||
)
|
||||
}
|
||||
|
||||
// buildField(idOff, idLen, fieldChar i32) → keyLen i32
|
||||
// Writes "a:<id>:<fieldChar>" into offScratch. Returns idLen+4.
|
||||
func buildFieldBody() []byte {
|
||||
return funcBody(
|
||||
noLocals,
|
||||
// scratch[0] = 'a'
|
||||
ic32(offScratch), ic32('a'), i32Store8(),
|
||||
// scratch[1] = ':'
|
||||
ic32(offScratch+1), ic32(':'), i32Store8(),
|
||||
// memcpy(scratch+2, idOff, idLen)
|
||||
ic32(offScratch+2), lget(0), lget(1), call(fnMemcpy),
|
||||
// scratch[2+idLen] = ':'
|
||||
ic32(offScratch+2), lget(1), i32Add(), ic32(':'), i32Store8(),
|
||||
// scratch[3+idLen] = fieldChar
|
||||
ic32(offScratch+3), lget(1), i32Add(), lget(2), i32Store8(),
|
||||
// return idLen + 4
|
||||
lget(1), ic32(4), i32Add(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Contract methods ──────────────────────────────────────────────────────────
|
||||
|
||||
// create(id string, title string, min_bid u64, duration u64)
|
||||
// Locals: idLen(0), titleLen(1), treasuryLen(2), keyLen(3)
|
||||
// i64 locals: minBid(4), duration(5)
|
||||
func createBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(4, tI32), localDecl(2, tI64)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
ic32(1), ic32(offArg1), ic32(128), call(fnGetArgStr), lset(1),
|
||||
lget(1), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// minBid = get_arg_u64(2)
|
||||
ic32(2), call(fnGetArgU64), lset(4),
|
||||
// duration = get_arg_u64(3)
|
||||
ic32(3), call(fnGetArgU64), lset(5),
|
||||
|
||||
// Check not already registered: get_state("a:<id>:x", ...)
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offReadStat), ic32(2), call(fnGetState),
|
||||
ic32(0), i32GtU(), if_(), // status exists → already created
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get treasury address
|
||||
ic32(offTreasury), ic32(64), call(fnGetContractTreasury), lset(2),
|
||||
|
||||
// Get caller (seller)
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(2), // reuse local 2 for callerLen
|
||||
|
||||
// Write seller: a:<id>:s → caller
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offCaller), lget(2), call(fnSetState),
|
||||
|
||||
// Write title: a:<id>:t → title
|
||||
ic32(offArg0), lget(0), ic32('t'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offArg1), lget(1), call(fnSetState),
|
||||
|
||||
// Write min_bid: a:<id>:m → put_u64
|
||||
ic32(offArg0), lget(0), ic32('m'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), lget(4), call(fnPutU64),
|
||||
|
||||
// Write end_block: a:<id>:e → get_block_height() + duration
|
||||
call(fnGetBlockHeight), lget(5), i64Add(),
|
||||
ic32(offArg0), lget(0), ic32('e'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3),
|
||||
// stack: keyPtr, keyLen, [endBlock i64 still on stack? No — need to rearrange]
|
||||
// Actually put_u64 takes (keyPtr, keyLen, val i64)
|
||||
// We already have endBlock on stack but called buildField after. Let me restructure.
|
||||
drop(), // drop keyLen from buildField - oops this won't work as coded above
|
||||
// Actually above I wrote lset(3) to save keyLen, so stack should be clean.
|
||||
// The i64 (endBlock) is on the operand stack before the buildField call... this is wrong.
|
||||
// Let me restructure: compute endBlock first into an i64 local.
|
||||
// I need another i64 local. Let me add local 6 (i64).
|
||||
// This requires restructuring... see revised body below
|
||||
)
|
||||
}
|
||||
|
||||
// Revised create body with proper local management
|
||||
// Locals: idLen(0), titleLen(1), callerLen(2), keyLen(3) [i32]
|
||||
// minBid(4), duration(5), endBlock(6) [i64]
|
||||
func createBodyV2() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(4, tI32), localDecl(3, tI64)),
|
||||
|
||||
// Read args
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
ic32(1), ic32(offArg1), ic32(128), call(fnGetArgStr), lset(1),
|
||||
lget(1), i32Eqz(), if_(), return_(), end_(),
|
||||
ic32(2), call(fnGetArgU64), lset(4), // minBid
|
||||
ic32(3), call(fnGetArgU64), lset(5), // duration
|
||||
|
||||
// Check id not already used: read status key
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offReadStat), ic32(2), call(fnGetState),
|
||||
ic32(0), i32GtU(), if_(),
|
||||
// ID already exists — log conflict
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get caller (seller)
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(2),
|
||||
|
||||
// Compute endBlock = get_block_height() + duration
|
||||
call(fnGetBlockHeight), lget(5), i64Add(), lset(6),
|
||||
|
||||
// Write seller: a:<id>:s → caller
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offCaller), lget(2), call(fnSetState),
|
||||
|
||||
// Write title: a:<id>:t → arg1
|
||||
ic32(offArg0), lget(0), ic32('t'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offArg1), lget(1), call(fnSetState),
|
||||
|
||||
// Write min_bid: a:<id>:m
|
||||
ic32(offArg0), lget(0), ic32('m'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), lget(4), call(fnPutU64),
|
||||
|
||||
// Write end_block: a:<id>:e
|
||||
ic32(offArg0), lget(0), ic32('e'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), lget(6), call(fnPutU64),
|
||||
|
||||
// Write top_bid = 0: a:<id>:v
|
||||
ic32(offArg0), lget(0), ic32('v'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic64(0), call(fnPutU64),
|
||||
|
||||
// Write status = 'o': a:<id>:x
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(3),
|
||||
ic32(offReadStat), ic32('o'), i32Store8(), // write 'o' to offReadStat
|
||||
ic32(offScratch), lget(3), ic32(offReadStat), ic32(1), call(fnSetState),
|
||||
|
||||
ic32(offCreatedPfx), ic32(9), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// bid(id string, amount u64)
|
||||
// Locals: idLen(0), callerLen(1), sellerLen(2), topBidderLen(3), keyLen(4) [i32]
|
||||
// amount(5), topBid(6), endBlock(7) [i64]
|
||||
func bidBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(5, tI32), localDecl(3, tI64)),
|
||||
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
ic32(1), call(fnGetArgU64), lset(5), // amount
|
||||
|
||||
// Check status == 'o'
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offReadStat), ic32(2), call(fnGetState),
|
||||
ic32(0), i32Ne(), if_(), // status byte exists
|
||||
ic32(offReadStat), i32Load8U(), ic32('o'), i32Ne(), if_(),
|
||||
ic32(offNotOpenPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
else_(),
|
||||
// no status byte = not found
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Check end_block > current block
|
||||
ic32(offArg0), lget(0), ic32('e'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), call(fnGetU64), lset(7),
|
||||
call(fnGetBlockHeight), lget(7), i64LeU(), if_(), // blockHeight >= endBlock → ended
|
||||
ic32(offNotOpenPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get current top_bid
|
||||
ic32(offArg0), lget(0), ic32('v'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), call(fnGetU64), lset(6),
|
||||
|
||||
// amount must be > max(topBid, minBid-1) → amount > topBid AND amount >= minBid
|
||||
// Check amount > topBid
|
||||
lget(5), lget(6), i64GtU(), i32Eqz(), if_(),
|
||||
ic32(offLowBidPfx), ic32(9), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get caller
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
|
||||
// Get treasury
|
||||
ic32(offTreasury), ic32(64), call(fnGetContractTreasury), lset(2), // reuse local 2
|
||||
|
||||
// Transfer amount from caller to treasury
|
||||
ic32(offCaller), lget(1), ic32(offTreasury), lget(2), lget(5), call(fnTransfer), drop(),
|
||||
|
||||
// Refund previous top bidder if topBid > 0
|
||||
lget(6), i64Eqz(), i32Eqz(), if_(), // topBid > 0
|
||||
// Read top_bidder address into offRead2
|
||||
ic32(offArg0), lget(0), ic32('b'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offRead2), ic32(128), call(fnGetState), lset(3),
|
||||
lget(3), ic32(0), i32GtU(), if_(), // topBidder address exists
|
||||
// transfer(treasury, topBidder, topBid)
|
||||
ic32(offTreasury), lget(2), ic32(offRead2), lget(3), lget(6), call(fnTransfer), drop(),
|
||||
end_(),
|
||||
end_(),
|
||||
|
||||
// Update top_bidder: a:<id>:b → caller
|
||||
ic32(offArg0), lget(0), ic32('b'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offCaller), lget(1), call(fnSetState),
|
||||
|
||||
// Update top_bid: a:<id>:v → amount
|
||||
ic32(offArg0), lget(0), ic32('v'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), lget(5), call(fnPutU64),
|
||||
|
||||
ic32(offBidPfx), ic32(5), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// settle(id string)
|
||||
// Locals: idLen(0), sellerLen(1), topBidderLen(2), treasuryLen(3), keyLen(4) [i32]
|
||||
// topBid(5), endBlock(6) [i64]
|
||||
func settleBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(5, tI32), localDecl(2, tI64)),
|
||||
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Check status == 'o'
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offReadStat), ic32(2), call(fnGetState),
|
||||
ic32(0), i32Ne(), if_(),
|
||||
ic32(offReadStat), i32Load8U(), ic32('o'), i32Ne(), if_(),
|
||||
ic32(offNotOpenPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
else_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Check auction has ended: current_block >= end_block
|
||||
ic32(offArg0), lget(0), ic32('e'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), call(fnGetU64), lset(6),
|
||||
call(fnGetBlockHeight), lget(6), i64LeU(), if_(), // height < endBlock → still open
|
||||
ic32(offStillOpenPfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get top_bid
|
||||
ic32(offArg0), lget(0), ic32('v'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), call(fnGetU64), lset(5),
|
||||
|
||||
// If top_bid > 0: transfer topBid from treasury to seller
|
||||
lget(5), i64Eqz(), i32Eqz(), if_(),
|
||||
// Read seller into offRead1
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offRead1), ic32(128), call(fnGetState), lset(1),
|
||||
// Get treasury
|
||||
ic32(offTreasury), ic32(64), call(fnGetContractTreasury), lset(3),
|
||||
ic32(offTreasury), lget(3), ic32(offRead1), lget(1), lget(5), call(fnTransfer), drop(),
|
||||
end_(),
|
||||
|
||||
// Mark status = 's'
|
||||
ic32(offReadStat), ic32('s'), i32Store8(),
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offReadStat), ic32(1), call(fnSetState),
|
||||
|
||||
ic32(offSettledPfx), ic32(9), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// cancel(id string) — seller cancels (no bids yet)
|
||||
// Locals: idLen(0), callerLen(1), sellerLen(2), keyLen(3) [i32]
|
||||
// topBid(4) [i64]
|
||||
func cancelBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(4, tI32), localDecl(1, tI64)),
|
||||
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Check status == 'o'
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offReadStat), ic32(2), call(fnGetState),
|
||||
ic32(0), i32Ne(), if_(),
|
||||
ic32(offReadStat), i32Load8U(), ic32('o'), i32Ne(), if_(),
|
||||
ic32(offNotOpenPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
else_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Check no bids: topBid == 0
|
||||
ic32(offArg0), lget(0), ic32('v'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), call(fnGetU64), lset(4),
|
||||
lget(4), i64Eqz(), i32Eqz(), if_(), // topBid > 0 → has bids
|
||||
ic32(offHasBidsPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Verify caller is seller
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offRead1), ic32(128), call(fnGetState), lset(2),
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
// isOwner: callerLen == sellerLen && bytes_equal
|
||||
lget(1), lget(2), i32Ne(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offCaller), ic32(offRead1), lget(1), call(fnBytesEqual),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Mark status = 'c'
|
||||
ic32(offReadStat), ic32('c'), i32Store8(),
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offReadStat), ic32(1), call(fnSetState),
|
||||
|
||||
ic32(offCancelledPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// info(id string) — log auction details
|
||||
// Locals: idLen(0), strLen(1), keyLen(2) [i32]
|
||||
// topBid(3) [i64]
|
||||
func infoBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(3, tI32), localDecl(1, tI64)),
|
||||
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Check exists
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(2),
|
||||
ic32(offScratch), lget(2), ic32(offReadStat), ic32(2), call(fnGetState),
|
||||
ic32(0), i32Ne(), i32Eqz(), if_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Log seller: a:<id>:s
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(2),
|
||||
ic32(offScratch), lget(2), ic32(offRead1), ic32(128), call(fnGetState), lset(1),
|
||||
ic32(offSellerPfx), ic32(8), ic32(offRead1), lget(1), call(fnLogPrefix),
|
||||
|
||||
// Log title: a:<id>:t
|
||||
ic32(offArg0), lget(0), ic32('t'), call(fnBuildField), lset(2),
|
||||
ic32(offScratch), lget(2), ic32(offRead1), ic32(128), call(fnGetState), lset(1),
|
||||
ic32(offTitlePfx), ic32(7), ic32(offRead1), lget(1), call(fnLogPrefix),
|
||||
|
||||
// Log status
|
||||
ic32(offReadStat), i32Load8U(), ic32('s'), i32Ne(), if_(),
|
||||
ic32(offReadStat), i32Load8U(), ic32('c'), i32Ne(), if_(),
|
||||
// status == 'o'
|
||||
ic32(offStatusPfx), ic32(8), ic32(offStrOpen), ic32(4), call(fnLogPrefix),
|
||||
else_(),
|
||||
ic32(offStatusPfx), ic32(8), ic32(offStrCancelled), ic32(9), call(fnLogPrefix),
|
||||
end_(),
|
||||
else_(),
|
||||
ic32(offStatusPfx), ic32(8), ic32(offStrSettled), ic32(7), call(fnLogPrefix),
|
||||
end_(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func main() {
|
||||
// Types:
|
||||
// 0: (i32,i32,i32)→(i32) get_arg_str, bytes_equal
|
||||
// 1: (i32)→(i64) get_arg_u64, get_block_height (no, diff sig)
|
||||
// 2: (i32,i32,i32,i32)→(i32) get_state
|
||||
// 3: (i32,i32,i32,i32)→() set_state, log_prefix
|
||||
// 4: (i32,i32)→() log
|
||||
// 5: (i32,i32,i32,i32,i64)→(i32) transfer
|
||||
// 6: (i32,i32)→(i64) get_balance, get_u64
|
||||
// 7: (i32,i32)→(i32) get_caller, get_contract_treasury
|
||||
// 8: ()→(i64) get_block_height
|
||||
// 9: (i32,i32,i64)→() put_u64
|
||||
// 10: ()→() exported methods
|
||||
// 11: (i32,i32,i32)→() memcpy
|
||||
// 12: (i32,i32,i32)→(i32) buildField
|
||||
typeSection := section(0x01, vec(
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{tI32}), // 0
|
||||
functype([]byte{tI32}, []byte{tI64}), // 1
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{tI32}), // 2
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{}), // 3
|
||||
functype([]byte{tI32, tI32}, []byte{}), // 4
|
||||
functype([]byte{tI32, tI32, tI32, tI32, tI64}, []byte{tI32}), // 5
|
||||
functype([]byte{tI32, tI32}, []byte{tI64}), // 6
|
||||
functype([]byte{tI32, tI32}, []byte{tI32}), // 7
|
||||
functype([]byte{}, []byte{tI64}), // 8
|
||||
functype([]byte{tI32, tI32, tI64}, []byte{}), // 9
|
||||
functype([]byte{}, []byte{}), // 10
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{}), // 11
|
||||
))
|
||||
|
||||
importSection := section(0x02, vec(
|
||||
importFunc("env", "get_arg_str", 0), // 0
|
||||
importFunc("env", "get_arg_u64", 1), // 1
|
||||
importFunc("env", "get_caller", 7), // 2
|
||||
importFunc("env", "get_state", 2), // 3
|
||||
importFunc("env", "set_state", 3), // 4
|
||||
importFunc("env", "log", 4), // 5
|
||||
importFunc("env", "transfer", 5), // 6
|
||||
importFunc("env", "get_balance", 6), // 7
|
||||
importFunc("env", "get_contract_treasury", 7), // 8
|
||||
importFunc("env", "get_block_height", 8), // 9
|
||||
importFunc("env", "put_u64", 9), // 10
|
||||
importFunc("env", "get_u64", 6), // 11
|
||||
))
|
||||
|
||||
// 9 local functions
|
||||
functionSection := section(0x03, vec(
|
||||
u(0), // bytes_equal type 0
|
||||
u(11), // memcpy type 11
|
||||
u(3), // log_prefix type 3
|
||||
u(0), // buildField type 0 (i32,i32,i32)→(i32)
|
||||
u(10), // create type 10
|
||||
u(10), // bid type 10
|
||||
u(10), // settle type 10
|
||||
u(10), // cancel type 10
|
||||
u(10), // info type 10
|
||||
))
|
||||
|
||||
memorySection := section(0x05, vec(cat([]byte{0x00}, u(2)))) // 2 pages
|
||||
|
||||
exportSection := section(0x07, vec(
|
||||
exportEntry("memory", 0x02, 0),
|
||||
exportEntry("create", 0x00, fnCreate),
|
||||
exportEntry("bid", 0x00, fnBid),
|
||||
exportEntry("settle", 0x00, fnSettle),
|
||||
exportEntry("cancel", 0x00, fnCancel),
|
||||
exportEntry("info", 0x00, fnInfo),
|
||||
))
|
||||
|
||||
dataSection := section(0x0B, cat(
|
||||
u(17),
|
||||
dataSegment(offCreatedPfx, []byte("created: ")),
|
||||
dataSegment(offBidPfx, []byte("bid: ")),
|
||||
dataSegment(offSettledPfx, []byte("settled: ")),
|
||||
dataSegment(offCancelledPfx, []byte("cancelled: ")),
|
||||
dataSegment(offUnauthPfx, []byte("unauthorized: ")),
|
||||
dataSegment(offNotFoundPfx, []byte("not found: ")),
|
||||
dataSegment(offNotOpenPfx, []byte("not open: ")),
|
||||
dataSegment(offHasBidsPfx, []byte("has bids: ")),
|
||||
dataSegment(offStillOpenPfx, []byte("still open: ")),
|
||||
dataSegment(offLowBidPfx, []byte("low bid: ")),
|
||||
dataSegment(offSellerPfx, []byte("seller: ")),
|
||||
dataSegment(offTitlePfx, []byte("title: ")),
|
||||
dataSegment(offTopBidPfx, []byte("top bid: ")),
|
||||
dataSegment(offEndBlockPfx, []byte("end block: ")),
|
||||
dataSegment(offStatusPfx, []byte("status: ")),
|
||||
dataSegment(offStrOpen, []byte("open")),
|
||||
dataSegment(offStrSettled, []byte("settled")),
|
||||
// Note: offStrCancelled is at 0x5B3 but we declared 17 segments
|
||||
// Add cancelled string too
|
||||
))
|
||||
// Fix: 18 data segments including "cancelled"
|
||||
dataSection = section(0x0B, cat(
|
||||
u(18),
|
||||
dataSegment(offCreatedPfx, []byte("created: ")),
|
||||
dataSegment(offBidPfx, []byte("bid: ")),
|
||||
dataSegment(offSettledPfx, []byte("settled: ")),
|
||||
dataSegment(offCancelledPfx, []byte("cancelled: ")),
|
||||
dataSegment(offUnauthPfx, []byte("unauthorized: ")),
|
||||
dataSegment(offNotFoundPfx, []byte("not found: ")),
|
||||
dataSegment(offNotOpenPfx, []byte("not open: ")),
|
||||
dataSegment(offHasBidsPfx, []byte("has bids: ")),
|
||||
dataSegment(offStillOpenPfx, []byte("still open: ")),
|
||||
dataSegment(offLowBidPfx, []byte("low bid: ")),
|
||||
dataSegment(offSellerPfx, []byte("seller: ")),
|
||||
dataSegment(offTitlePfx, []byte("title: ")),
|
||||
dataSegment(offTopBidPfx, []byte("top bid: ")),
|
||||
dataSegment(offEndBlockPfx, []byte("end block: ")),
|
||||
dataSegment(offStatusPfx, []byte("status: ")),
|
||||
dataSegment(offStrOpen, []byte("open")),
|
||||
dataSegment(offStrSettled, []byte("settled")),
|
||||
dataSegment(offStrCancelled, []byte("cancelled")),
|
||||
))
|
||||
|
||||
codeSection := section(0x0A, cat(
|
||||
u(9),
|
||||
bytesEqualBody(),
|
||||
memcpyBody(),
|
||||
logPrefixBody(),
|
||||
buildFieldBody(),
|
||||
createBodyV2(),
|
||||
bidBody(),
|
||||
settleBody(),
|
||||
cancelBody(),
|
||||
infoBody(),
|
||||
))
|
||||
|
||||
module := cat(
|
||||
[]byte{0x00, 0x61, 0x73, 0x6d},
|
||||
[]byte{0x01, 0x00, 0x00, 0x00},
|
||||
typeSection,
|
||||
importSection,
|
||||
functionSection,
|
||||
memorySection,
|
||||
exportSection,
|
||||
dataSection,
|
||||
codeSection,
|
||||
)
|
||||
|
||||
out := "contracts/auction/auction.wasm"
|
||||
if err := os.WriteFile(out, module, 0644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "write:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Written %s (%d bytes)\n", out, len(module))
|
||||
}
|
||||
BIN
contracts/counter/counter.wasm
Normal file
BIN
contracts/counter/counter.wasm
Normal file
Binary file not shown.
102
contracts/counter/counter.wat
Normal file
102
contracts/counter/counter.wat
Normal file
@@ -0,0 +1,102 @@
|
||||
(module
|
||||
;; Counter smart contract — methods: increment, get, reset
|
||||
;; State key "counter" stores uint64 as 8-byte big-endian.
|
||||
;; State key "owner" stores the deployer pub key set on first reset() call.
|
||||
|
||||
;; ── imports ──────────────────────────────────────────────────────────────
|
||||
(import "env" "put_u64"
|
||||
(func $put_u64 (param i32 i32 i64)))
|
||||
(import "env" "get_u64"
|
||||
(func $get_u64 (param i32 i32) (result i64)))
|
||||
(import "env" "get_caller"
|
||||
(func $get_caller (param i32 i32) (result i32)))
|
||||
(import "env" "get_state"
|
||||
(func $get_state (param i32 i32 i32 i32) (result i32)))
|
||||
(import "env" "set_state"
|
||||
(func $set_state (param i32 i32 i32 i32)))
|
||||
(import "env" "log"
|
||||
(func $log (param i32 i32)))
|
||||
|
||||
;; ── memory ───────────────────────────────────────────────────────────────
|
||||
;; Memory layout:
|
||||
;; offset 0 : "counter" (7 bytes) — state key for the counter value
|
||||
;; offset 16 : "owner" (5 bytes) — state key for the owner pub key
|
||||
;; offset 32 : caller buffer (128 bytes)
|
||||
;; offset 160 : owner buffer (128 bytes)
|
||||
;; offset 288 : log messages
|
||||
(memory (export "memory") 1)
|
||||
|
||||
(data (i32.const 0) "counter")
|
||||
(data (i32.const 16) "owner")
|
||||
(data (i32.const 288) "incremented")
|
||||
(data (i32.const 300) "get called")
|
||||
(data (i32.const 310) "reset")
|
||||
(data (i32.const 316) "unauthorized")
|
||||
(data (i32.const 329) "reset ok")
|
||||
|
||||
;; ── increment() ──────────────────────────────────────────────────────────
|
||||
(func (export "increment")
|
||||
(local $val i64)
|
||||
(local.set $val (call $get_u64 (i32.const 0) (i32.const 7)))
|
||||
(local.set $val (i64.add (local.get $val) (i64.const 1)))
|
||||
(call $put_u64 (i32.const 0) (i32.const 7) (local.get $val))
|
||||
(call $log (i32.const 288) (i32.const 11))
|
||||
)
|
||||
|
||||
;; ── get() ─────────────────────────────────────────────────────────────────
|
||||
(func (export "get")
|
||||
(call $log (i32.const 300) (i32.const 10))
|
||||
)
|
||||
|
||||
;; ── reset() ───────────────────────────────────────────────────────────────
|
||||
;; Resets counter to 0. Only callable by the deployer (first caller sets ownership).
|
||||
(func (export "reset")
|
||||
(local $callerLen i32)
|
||||
(local $ownerLen i32)
|
||||
(local $i i32)
|
||||
(local $same i32)
|
||||
|
||||
(local.set $callerLen (call $get_caller (i32.const 32) (i32.const 128)))
|
||||
(local.set $ownerLen
|
||||
(call $get_state (i32.const 16) (i32.const 5) (i32.const 160) (i32.const 128)))
|
||||
|
||||
;; No owner yet — first caller becomes owner and resets to 0
|
||||
(if (i32.eqz (local.get $ownerLen))
|
||||
(then
|
||||
(call $set_state (i32.const 16) (i32.const 5) (i32.const 32) (local.get $callerLen))
|
||||
(call $put_u64 (i32.const 0) (i32.const 7) (i64.const 0))
|
||||
(call $log (i32.const 329) (i32.const 8))
|
||||
return
|
||||
)
|
||||
)
|
||||
|
||||
;; Length mismatch → unauthorized
|
||||
(if (i32.ne (local.get $callerLen) (local.get $ownerLen))
|
||||
(then (call $log (i32.const 316) (i32.const 12)) return)
|
||||
)
|
||||
|
||||
;; Byte-by-byte comparison
|
||||
(local.set $same (i32.const 1))
|
||||
(local.set $i (i32.const 0))
|
||||
(block $break
|
||||
(loop $loop
|
||||
(br_if $break (i32.ge_u (local.get $i) (local.get $callerLen)))
|
||||
(if (i32.ne
|
||||
(i32.load8_u (i32.add (i32.const 32) (local.get $i)))
|
||||
(i32.load8_u (i32.add (i32.const 160) (local.get $i))))
|
||||
(then (local.set $same (i32.const 0)) (br $break))
|
||||
)
|
||||
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
||||
(br $loop)
|
||||
)
|
||||
)
|
||||
|
||||
(if (i32.eqz (local.get $same))
|
||||
(then (call $log (i32.const 316) (i32.const 12)) return)
|
||||
)
|
||||
|
||||
;; Authorized — reset
|
||||
(call $put_u64 (i32.const 0) (i32.const 7) (i64.const 0))
|
||||
(call $log (i32.const 329) (i32.const 8))
|
||||
)
|
||||
)
|
||||
1
contracts/counter/counter_abi.json
Normal file
1
contracts/counter/counter_abi.json
Normal file
@@ -0,0 +1 @@
|
||||
{"methods":[{"name":"increment","args":[]},{"name":"get","args":[]},{"name":"reset","args":[]}]}
|
||||
331
contracts/counter/gen/main.go
Normal file
331
contracts/counter/gen/main.go
Normal file
@@ -0,0 +1,331 @@
|
||||
// gen generates contracts/counter/counter.wasm — binary WASM for the counter contract.
|
||||
// Run from the repo root: go run ./contracts/counter/gen/
|
||||
//
|
||||
// Contract methods exported: increment, get, reset
|
||||
// Host imports from "env": put_u64, get_u64, log, get_caller, get_state, set_state
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ── LEB128 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func u(v uint64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
if v != 0 {
|
||||
bt |= 0x80
|
||||
}
|
||||
b = append(b, bt)
|
||||
if v == 0 {
|
||||
return b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func s(v int64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
sign := (bt & 0x40) != 0
|
||||
if (v == 0 && !sign) || (v == -1 && sign) {
|
||||
return append(b, bt)
|
||||
}
|
||||
b = append(b, bt|0x80)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Builders ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func cat(slices ...[]byte) []byte {
|
||||
var out []byte
|
||||
for _, s := range slices {
|
||||
out = append(out, s...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func wstr(str string) []byte { return cat(u(uint64(len(str))), []byte(str)) }
|
||||
|
||||
func section(id byte, content []byte) []byte {
|
||||
return cat([]byte{id}, u(uint64(len(content))), content)
|
||||
}
|
||||
|
||||
// vec encodes a vector: count followed by concatenated items.
|
||||
func vec(items ...[]byte) []byte {
|
||||
out := u(uint64(len(items)))
|
||||
for _, it := range items {
|
||||
out = append(out, it...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// functype encodes a WASM function type (0x60 prefix).
|
||||
func functype(params, results []byte) []byte {
|
||||
return cat([]byte{0x60}, u(uint64(len(params))), params, u(uint64(len(results))), results)
|
||||
}
|
||||
|
||||
// importFunc encodes a function import entry.
|
||||
func importFunc(mod, name string, typeIdx uint32) []byte {
|
||||
return cat(wstr(mod), wstr(name), []byte{0x00}, u(uint64(typeIdx)))
|
||||
}
|
||||
|
||||
// exportEntry encodes an export entry.
|
||||
func exportEntry(name string, kind byte, idx uint32) []byte {
|
||||
return cat(wstr(name), []byte{kind}, u(uint64(idx)))
|
||||
}
|
||||
|
||||
// dataSegment encodes an active data segment for memory 0.
|
||||
func dataSegment(offset int32, data []byte) []byte {
|
||||
return cat(
|
||||
[]byte{0x00}, // active segment, implicit mem 0
|
||||
[]byte{0x41}, s(int64(offset)), []byte{0x0B}, // i32.const offset; end
|
||||
u(uint64(len(data))), data,
|
||||
)
|
||||
}
|
||||
|
||||
// funcBody encodes one function body: localDecls + instructions + end.
|
||||
func funcBody(localDecls []byte, instrs ...[]byte) []byte {
|
||||
inner := cat(localDecls)
|
||||
for _, ins := range instrs {
|
||||
inner = append(inner, ins...)
|
||||
}
|
||||
inner = append(inner, 0x0B) // end
|
||||
return cat(u(uint64(len(inner))), inner)
|
||||
}
|
||||
|
||||
// noLocals is an empty local decl list.
|
||||
var noLocals = u(0)
|
||||
|
||||
// localDecl encodes n locals of a given type.
|
||||
func localDecl(n uint32, typ byte) []byte { return cat(u(uint64(n)), []byte{typ}) }
|
||||
func withLocals(decls ...[]byte) []byte {
|
||||
return cat(u(uint64(len(decls))), cat(decls...))
|
||||
}
|
||||
|
||||
// ── Instructions ──────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
tI32 byte = 0x7F
|
||||
tI64 byte = 0x7E
|
||||
)
|
||||
|
||||
func call(fn uint32) []byte { return cat([]byte{0x10}, u(uint64(fn))) }
|
||||
func lget(i uint32) []byte { return cat([]byte{0x20}, u(uint64(i))) }
|
||||
func lset(i uint32) []byte { return cat([]byte{0x21}, u(uint64(i))) }
|
||||
func ic32(v int32) []byte { return cat([]byte{0x41}, s(int64(v))) }
|
||||
func ic64(v int64) []byte { return cat([]byte{0x42}, s(v)) }
|
||||
func block_() []byte { return []byte{0x02, 0x40} }
|
||||
func loop_() []byte { return []byte{0x03, 0x40} }
|
||||
func if_() []byte { return []byte{0x04, 0x40} }
|
||||
func end_() []byte { return []byte{0x0B} }
|
||||
func br_(lbl uint32) []byte { return cat([]byte{0x0C}, u(uint64(lbl))) }
|
||||
func brIf_(lbl uint32) []byte { return cat([]byte{0x0D}, u(uint64(lbl))) }
|
||||
func return_() []byte { return []byte{0x0F} }
|
||||
func i32Eqz() []byte { return []byte{0x45} }
|
||||
func i32Ne() []byte { return []byte{0x47} }
|
||||
func i32GeU() []byte { return []byte{0x4F} }
|
||||
func i32Add() []byte { return []byte{0x6A} }
|
||||
func i64Add() []byte { return []byte{0x7C} }
|
||||
func i32Load8U() []byte { return []byte{0x2D, 0x00, 0x00} } // align=0, offset=0
|
||||
|
||||
// ── Memory layout constants ───────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
offCounter = 0x00 // "counter" (7 bytes)
|
||||
offOwner = 0x10 // "owner" (5 bytes)
|
||||
offIncMsg = 0x20 // "incremented" (11 bytes)
|
||||
offGetMsg = 0x30 // "get called" (10 bytes)
|
||||
offResetOk = 0x40 // "reset ok" (8 bytes)
|
||||
offUnauth = 0x50 // "unauthorized" (12 bytes)
|
||||
offCallerBuf = 0x60 // caller buf (128 bytes)
|
||||
offOwnerBuf = 0xE0 // owner buf (128 bytes)
|
||||
)
|
||||
|
||||
// Import function indices
|
||||
const (
|
||||
fnPutU64 = 0 // put_u64(keyPtr, keyLen i32, val i64)
|
||||
fnGetU64 = 1 // get_u64(keyPtr, keyLen i32) → i64
|
||||
fnLog = 2 // log(msgPtr, msgLen i32)
|
||||
fnGetCaller = 3 // get_caller(bufPtr, bufLen i32) → i32
|
||||
fnGetState = 4 // get_state(kPtr,kLen,dPtr,dLen i32) → i32
|
||||
fnSetState = 5 // set_state(kPtr,kLen,vPtr,vLen i32)
|
||||
)
|
||||
|
||||
// Local function indices (imports are 0-5, locals start at 6)
|
||||
const (
|
||||
fnIncrement = 6
|
||||
fnGet = 7
|
||||
fnReset = 8
|
||||
)
|
||||
|
||||
func main() {
|
||||
// ── Type section ─────────────────────────────────────────────────────────
|
||||
// Type 0: (i32,i32,i64)→() put_u64
|
||||
// Type 1: (i32,i32)→(i64) get_u64
|
||||
// Type 2: (i32,i32)→() log
|
||||
// Type 3: (i32,i32)→(i32) get_caller
|
||||
// Type 4: (i32,i32,i32,i32)→(i32) get_state
|
||||
// Type 5: (i32,i32,i32,i32)→() set_state
|
||||
// Type 6: ()→() increment, get, reset
|
||||
typeSection := section(0x01, vec(
|
||||
functype([]byte{tI32, tI32, tI64}, []byte{}), // 0
|
||||
functype([]byte{tI32, tI32}, []byte{tI64}), // 1
|
||||
functype([]byte{tI32, tI32}, []byte{}), // 2
|
||||
functype([]byte{tI32, tI32}, []byte{tI32}), // 3
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{tI32}), // 4
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{}), // 5
|
||||
functype([]byte{}, []byte{}), // 6
|
||||
))
|
||||
|
||||
// ── Import section ────────────────────────────────────────────────────────
|
||||
importSection := section(0x02, vec(
|
||||
importFunc("env", "put_u64", fnPutU64),
|
||||
importFunc("env", "get_u64", fnGetU64),
|
||||
importFunc("env", "log", fnLog),
|
||||
importFunc("env", "get_caller", fnGetCaller),
|
||||
importFunc("env", "get_state", fnGetState),
|
||||
importFunc("env", "set_state", fnSetState),
|
||||
))
|
||||
|
||||
// ── Function section: 3 local functions, all type 6 ──────────────────────
|
||||
functionSection := section(0x03, vec(u(6), u(6), u(6)))
|
||||
|
||||
// ── Memory section: 1 page (64 KiB) ──────────────────────────────────────
|
||||
// limits type 0x00 = min only; type 0x01 = min+max
|
||||
memorySection := section(0x05, vec(cat([]byte{0x00}, u(1)))) // min=1, no max
|
||||
|
||||
// ── Export section ────────────────────────────────────────────────────────
|
||||
exportSection := section(0x07, vec(
|
||||
exportEntry("memory", 0x02, 0),
|
||||
exportEntry("increment", 0x00, fnIncrement),
|
||||
exportEntry("get", 0x00, fnGet),
|
||||
exportEntry("reset", 0x00, fnReset),
|
||||
))
|
||||
|
||||
// ── Data section ──────────────────────────────────────────────────────────
|
||||
dataSection := section(0x0B, cat(
|
||||
u(6), // 6 segments
|
||||
dataSegment(offCounter, []byte("counter")),
|
||||
dataSegment(offOwner, []byte("owner")),
|
||||
dataSegment(offIncMsg, []byte("incremented")),
|
||||
dataSegment(offGetMsg, []byte("get called")),
|
||||
dataSegment(offResetOk, []byte("reset ok")),
|
||||
dataSegment(offUnauth, []byte("unauthorized")),
|
||||
))
|
||||
|
||||
// ── Code section ─────────────────────────────────────────────────────────
|
||||
|
||||
// increment():
|
||||
// local $val i64
|
||||
// $val = get_u64("counter")
|
||||
// $val++
|
||||
// put_u64("counter", $val)
|
||||
// log("incremented")
|
||||
incrementBody := funcBody(
|
||||
withLocals(localDecl(1, tI64)),
|
||||
ic32(offCounter), ic32(7), call(fnGetU64), lset(0),
|
||||
lget(0), ic64(1), i64Add(), lset(0),
|
||||
ic32(offCounter), ic32(7), lget(0), call(fnPutU64),
|
||||
ic32(offIncMsg), ic32(11), call(fnLog),
|
||||
)
|
||||
|
||||
// get():
|
||||
// log("get called")
|
||||
getBody := funcBody(
|
||||
noLocals,
|
||||
ic32(offGetMsg), ic32(10), call(fnLog),
|
||||
)
|
||||
|
||||
// reset():
|
||||
// locals: callerLen(0), ownerLen(1), i(2), same(3) — all i32
|
||||
// callerLen = get_caller(callerBuf, 128)
|
||||
// ownerLen = get_state("owner", ownerBuf, 128)
|
||||
// if ownerLen == 0:
|
||||
// set_state("owner", callerBuf[:callerLen])
|
||||
// put_u64("counter", 0)
|
||||
// log("reset ok")
|
||||
// return
|
||||
// if callerLen != ownerLen: log unauthorized; return
|
||||
// same = 1; i = 0
|
||||
// block:
|
||||
// loop:
|
||||
// if i >= callerLen: br 1 (exit block)
|
||||
// if callerBuf[i] != ownerBuf[i]: same=0; br 1
|
||||
// i++; continue loop
|
||||
// if !same: log unauthorized; return
|
||||
// put_u64("counter", 0); log("reset ok")
|
||||
resetBody := funcBody(
|
||||
withLocals(localDecl(4, tI32)),
|
||||
// callerLen = get_caller(callerBuf, 128)
|
||||
ic32(offCallerBuf), ic32(128), call(fnGetCaller), lset(0),
|
||||
// ownerLen = get_state("owner", 5, ownerBuf, 128)
|
||||
ic32(offOwner), ic32(5), ic32(offOwnerBuf), ic32(128), call(fnGetState), lset(1),
|
||||
// if ownerLen == 0:
|
||||
lget(1), i32Eqz(), if_(),
|
||||
ic32(offOwner), ic32(5), ic32(offCallerBuf), lget(0), call(fnSetState),
|
||||
ic32(offCounter), ic32(7), ic64(0), call(fnPutU64),
|
||||
ic32(offResetOk), ic32(8), call(fnLog),
|
||||
return_(),
|
||||
end_(),
|
||||
// if callerLen != ownerLen: unauthorized
|
||||
lget(0), lget(1), i32Ne(), if_(),
|
||||
ic32(offUnauth), ic32(12), call(fnLog),
|
||||
return_(),
|
||||
end_(),
|
||||
// same = 1; i = 0
|
||||
ic32(1), lset(3),
|
||||
ic32(0), lset(2),
|
||||
// block $break
|
||||
block_(),
|
||||
loop_(),
|
||||
lget(2), lget(0), i32GeU(), brIf_(1), // i >= callerLen → break
|
||||
// load callerBuf[i]
|
||||
ic32(offCallerBuf), lget(2), i32Add(), i32Load8U(),
|
||||
// load ownerBuf[i]
|
||||
ic32(offOwnerBuf), lget(2), i32Add(), i32Load8U(),
|
||||
i32Ne(), if_(),
|
||||
ic32(0), lset(3),
|
||||
br_(2), // break out of block
|
||||
end_(),
|
||||
lget(2), ic32(1), i32Add(), lset(2),
|
||||
br_(0), // continue loop
|
||||
end_(),
|
||||
end_(),
|
||||
// if !same: unauthorized
|
||||
lget(3), i32Eqz(), if_(),
|
||||
ic32(offUnauth), ic32(12), call(fnLog),
|
||||
return_(),
|
||||
end_(),
|
||||
// authorized
|
||||
ic32(offCounter), ic32(7), ic64(0), call(fnPutU64),
|
||||
ic32(offResetOk), ic32(8), call(fnLog),
|
||||
)
|
||||
|
||||
codeSection := section(0x0A, cat(u(3), incrementBody, getBody, resetBody))
|
||||
|
||||
// ── Assemble module ───────────────────────────────────────────────────────
|
||||
module := cat(
|
||||
[]byte{0x00, 0x61, 0x73, 0x6d}, // magic \0asm
|
||||
[]byte{0x01, 0x00, 0x00, 0x00}, // version 1
|
||||
typeSection,
|
||||
importSection,
|
||||
functionSection,
|
||||
memorySection,
|
||||
exportSection,
|
||||
dataSection,
|
||||
codeSection,
|
||||
)
|
||||
|
||||
out := "contracts/counter/counter.wasm"
|
||||
if err := os.WriteFile(out, module, 0644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "write:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Written %s (%d bytes)\n", out, len(module))
|
||||
}
|
||||
137
contracts/counter/main.go
Normal file
137
contracts/counter/main.go
Normal file
@@ -0,0 +1,137 @@
|
||||
// Counter smart contract — compiles to WASM with GOOS=wasip1 GOARCH=wasm.
|
||||
//
|
||||
// Methods (exported via //go:export):
|
||||
// - increment — adds 1 to the stored counter
|
||||
// - get — logs the current value (readable via /api/contracts/{id}/state/counter)
|
||||
// - reset — resets counter to 0; only the first caller (owner) is allowed
|
||||
//
|
||||
// Host imports from the "env" module (see vm/host.go):
|
||||
// - put_u64(keyPtr, keyLen, val) — stores uint64 as 8-byte big-endian
|
||||
// - get_u64(keyPtr, keyLen) uint64 — reads 8-byte big-endian uint64
|
||||
// - get_caller(buf, bufLen) int32 — writes caller pub key hex into buf
|
||||
// - get_state(kPtr,kLen,dPtr,dLen) int32 — reads raw state bytes
|
||||
// - set_state(kPtr,kLen,vPtr,vLen) — writes raw state bytes
|
||||
// - log(msgPtr, msgLen) — emits message to node log
|
||||
//
|
||||
//go:build wasip1
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// ── host function imports ─────────────────────────────────────────────────────
|
||||
|
||||
//go:wasmimport env put_u64
|
||||
func hostPutU64(keyPtr unsafe.Pointer, keyLen int32, val uint64)
|
||||
|
||||
//go:wasmimport env get_u64
|
||||
func hostGetU64(keyPtr unsafe.Pointer, keyLen int32) uint64
|
||||
|
||||
//go:wasmimport env get_caller
|
||||
func hostGetCaller(buf unsafe.Pointer, bufLen int32) int32
|
||||
|
||||
//go:wasmimport env get_state
|
||||
func hostGetState(keyPtr unsafe.Pointer, keyLen int32, dstPtr unsafe.Pointer, dstLen int32) int32
|
||||
|
||||
//go:wasmimport env set_state
|
||||
func hostSetState(keyPtr unsafe.Pointer, keyLen int32, valPtr unsafe.Pointer, valLen int32)
|
||||
|
||||
//go:wasmimport env log
|
||||
func hostLog(msgPtr unsafe.Pointer, msgLen int32)
|
||||
|
||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func logMsg(s string) {
|
||||
if len(s) == 0 {
|
||||
return
|
||||
}
|
||||
b := []byte(s)
|
||||
hostLog(unsafe.Pointer(&b[0]), int32(len(b)))
|
||||
}
|
||||
|
||||
func putU64(key string, val uint64) {
|
||||
b := []byte(key)
|
||||
hostPutU64(unsafe.Pointer(&b[0]), int32(len(b)), val)
|
||||
}
|
||||
|
||||
func getU64(key string) uint64 {
|
||||
b := []byte(key)
|
||||
return hostGetU64(unsafe.Pointer(&b[0]), int32(len(b)))
|
||||
}
|
||||
|
||||
func getState(key string, dst []byte) int32 {
|
||||
kb := []byte(key)
|
||||
return hostGetState(unsafe.Pointer(&kb[0]), int32(len(kb)),
|
||||
unsafe.Pointer(&dst[0]), int32(len(dst)))
|
||||
}
|
||||
|
||||
func setState(key string, val []byte) {
|
||||
kb := []byte(key)
|
||||
hostSetState(unsafe.Pointer(&kb[0]), int32(len(kb)),
|
||||
unsafe.Pointer(&val[0]), int32(len(val)))
|
||||
}
|
||||
|
||||
func getCaller() string {
|
||||
buf := make([]byte, 128)
|
||||
n := hostGetCaller(unsafe.Pointer(&buf[0]), int32(len(buf)))
|
||||
if n <= 0 {
|
||||
return ""
|
||||
}
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
// ── contract state keys ───────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
keyCounter = "counter"
|
||||
keyOwner = "owner"
|
||||
)
|
||||
|
||||
// ── exported contract methods ─────────────────────────────────────────────────
|
||||
|
||||
//go:export increment
|
||||
func increment() {
|
||||
val := getU64(keyCounter)
|
||||
val++
|
||||
putU64(keyCounter, val)
|
||||
logMsg("incremented")
|
||||
}
|
||||
|
||||
//go:export get
|
||||
func get() {
|
||||
logMsg("get called")
|
||||
}
|
||||
|
||||
//go:export reset
|
||||
func reset() {
|
||||
caller := getCaller()
|
||||
if caller == "" {
|
||||
logMsg("reset: no caller")
|
||||
return
|
||||
}
|
||||
|
||||
ownerBuf := make([]byte, 128)
|
||||
ownerLen := getState(keyOwner, ownerBuf)
|
||||
|
||||
if ownerLen == 0 {
|
||||
// No owner set yet — first caller becomes the owner.
|
||||
setState(keyOwner, []byte(caller))
|
||||
putU64(keyCounter, 0)
|
||||
logMsg("reset ok (owner set)")
|
||||
return
|
||||
}
|
||||
|
||||
owner := string(ownerBuf[:ownerLen])
|
||||
if caller != owner {
|
||||
logMsg("reset: unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
putU64(keyCounter, 0)
|
||||
logMsg("reset ok")
|
||||
}
|
||||
|
||||
// main is required by the Go runtime for wasip1 programs.
|
||||
func main() {}
|
||||
BIN
contracts/escrow/escrow.wasm
Normal file
BIN
contracts/escrow/escrow.wasm
Normal file
Binary file not shown.
57
contracts/escrow/escrow_abi.json
Normal file
57
contracts/escrow/escrow_abi.json
Normal file
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"contract": "escrow",
|
||||
"version": "1.0.0",
|
||||
"description": "Two-party trustless escrow. Buyer deposits funds into the contract treasury. Seller delivers; buyer releases. If disputed, the contract admin resolves.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "init",
|
||||
"description": "Set the caller as the escrow admin. Call once after deployment.",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "create",
|
||||
"description": "Buyer creates an escrow. Transfers amount from buyer to treasury. Logs 'created: <id>'.",
|
||||
"args": [
|
||||
{"name": "id", "type": "string", "description": "Unique escrow ID (user-supplied)"},
|
||||
{"name": "seller", "type": "string", "description": "Seller address (hex pubkey)"},
|
||||
{"name": "amount", "type": "uint64", "description": "Amount in µT to lock in escrow"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "release",
|
||||
"description": "Buyer releases funds to seller. Transfers treasury → seller. Logs 'released: <id>'.",
|
||||
"args": [
|
||||
{"name": "id", "type": "string", "description": "Escrow ID"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "refund",
|
||||
"description": "Seller refunds the buyer (voluntary). Transfers treasury → buyer. Logs 'refunded: <id>'.",
|
||||
"args": [
|
||||
{"name": "id", "type": "string", "description": "Escrow ID"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dispute",
|
||||
"description": "Buyer or seller raises a dispute. Logs 'disputed: <id>'. Admin must then call resolve.",
|
||||
"args": [
|
||||
{"name": "id", "type": "string", "description": "Escrow ID"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "resolve",
|
||||
"description": "Admin resolves a disputed escrow. winner must be 'buyer' or 'seller'. Logs 'resolved: <id>'.",
|
||||
"args": [
|
||||
{"name": "id", "type": "string", "description": "Escrow ID"},
|
||||
{"name": "winner", "type": "string", "description": "'buyer' or 'seller'"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "info",
|
||||
"description": "Log escrow details: buyer, seller, amount, status.",
|
||||
"args": [
|
||||
{"name": "id", "type": "string", "description": "Escrow ID"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
818
contracts/escrow/gen/main.go
Normal file
818
contracts/escrow/gen/main.go
Normal file
@@ -0,0 +1,818 @@
|
||||
// gen generates contracts/escrow/escrow.wasm
|
||||
// Run from repo root: go run ./contracts/escrow/gen/
|
||||
//
|
||||
// Methods: init, create, release, refund, dispute, resolve, info
|
||||
//
|
||||
// State keys (per escrow id):
|
||||
// "admin" → admin address
|
||||
// "e:<id>:b" → buyer address
|
||||
// "e:<id>:s" → seller address
|
||||
// "e:<id>:v" → amount (8-byte big-endian u64)
|
||||
// "e:<id>:x" → status byte:
|
||||
// 'a' = active (funds locked)
|
||||
// 'd' = disputed
|
||||
// 'r' = released to seller
|
||||
// 'f' = refunded to buyer
|
||||
//
|
||||
// The contract treasury holds the locked funds during active/disputed state.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ── LEB128 & builders ────────────────────────────────────────────────────────
|
||||
|
||||
func u(v uint64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
if v != 0 {
|
||||
bt |= 0x80
|
||||
}
|
||||
b = append(b, bt)
|
||||
if v == 0 {
|
||||
return b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func s(v int64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
sign := (bt & 0x40) != 0
|
||||
if (v == 0 && !sign) || (v == -1 && sign) {
|
||||
return append(b, bt)
|
||||
}
|
||||
b = append(b, bt|0x80)
|
||||
}
|
||||
}
|
||||
|
||||
func cat(slices ...[]byte) []byte {
|
||||
var out []byte
|
||||
for _, sl := range slices {
|
||||
out = append(out, sl...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func wstr(str string) []byte { return cat(u(uint64(len(str))), []byte(str)) }
|
||||
|
||||
func section(id byte, content []byte) []byte {
|
||||
return cat([]byte{id}, u(uint64(len(content))), content)
|
||||
}
|
||||
|
||||
func vec(items ...[]byte) []byte {
|
||||
out := u(uint64(len(items)))
|
||||
for _, it := range items {
|
||||
out = append(out, it...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func functype(params, results []byte) []byte {
|
||||
return cat([]byte{0x60}, u(uint64(len(params))), params, u(uint64(len(results))), results)
|
||||
}
|
||||
|
||||
func importFunc(mod, name string, typeIdx uint32) []byte {
|
||||
return cat(wstr(mod), wstr(name), []byte{0x00}, u(uint64(typeIdx)))
|
||||
}
|
||||
|
||||
func exportEntry(name string, kind byte, idx uint32) []byte {
|
||||
return cat(wstr(name), []byte{kind}, u(uint64(idx)))
|
||||
}
|
||||
|
||||
func dataSegment(offset int32, data []byte) []byte {
|
||||
return cat(
|
||||
[]byte{0x00},
|
||||
[]byte{0x41}, s(int64(offset)), []byte{0x0B},
|
||||
u(uint64(len(data))), data,
|
||||
)
|
||||
}
|
||||
|
||||
func funcBody(localDecls []byte, instrs ...[]byte) []byte {
|
||||
inner := cat(localDecls)
|
||||
for _, ins := range instrs {
|
||||
inner = append(inner, ins...)
|
||||
}
|
||||
inner = append(inner, 0x0B)
|
||||
return cat(u(uint64(len(inner))), inner)
|
||||
}
|
||||
|
||||
var noLocals = u(0)
|
||||
|
||||
func localDecl(n uint32, typ byte) []byte { return cat(u(uint64(n)), []byte{typ}) }
|
||||
func withLocals(decls ...[]byte) []byte {
|
||||
return cat(u(uint64(len(decls))), cat(decls...))
|
||||
}
|
||||
|
||||
const (
|
||||
tI32 byte = 0x7F
|
||||
tI64 byte = 0x7E
|
||||
)
|
||||
|
||||
func call(fn uint32) []byte { return cat([]byte{0x10}, u(uint64(fn))) }
|
||||
func lget(i uint32) []byte { return cat([]byte{0x20}, u(uint64(i))) }
|
||||
func lset(i uint32) []byte { return cat([]byte{0x21}, u(uint64(i))) }
|
||||
func ic32(v int32) []byte { return cat([]byte{0x41}, s(int64(v))) }
|
||||
func ic64(v int64) []byte { return cat([]byte{0x42}, s(v)) }
|
||||
func block_() []byte { return []byte{0x02, 0x40} }
|
||||
func loop_() []byte { return []byte{0x03, 0x40} }
|
||||
func if_() []byte { return []byte{0x04, 0x40} }
|
||||
func ifI32() []byte { return []byte{0x04, tI32} }
|
||||
func else_() []byte { return []byte{0x05} }
|
||||
func end_() []byte { return []byte{0x0B} }
|
||||
func br_(lbl uint32) []byte { return cat([]byte{0x0C}, u(uint64(lbl))) }
|
||||
func brIf_(lbl uint32) []byte { return cat([]byte{0x0D}, u(uint64(lbl))) }
|
||||
func return_() []byte { return []byte{0x0F} }
|
||||
func drop() []byte { return []byte{0x1A} }
|
||||
func i32Eqz() []byte { return []byte{0x45} }
|
||||
func i32Ne() []byte { return []byte{0x47} }
|
||||
func i32GtU() []byte { return []byte{0x4B} }
|
||||
func i32GeU() []byte { return []byte{0x4F} }
|
||||
func i32Add() []byte { return []byte{0x6A} }
|
||||
func i64Eqz() []byte { return []byte{0x50} }
|
||||
func i32Load8U() []byte { return []byte{0x2D, 0x00, 0x00} }
|
||||
func i32Store8() []byte { return []byte{0x3A, 0x00, 0x00} }
|
||||
|
||||
// ── Memory layout ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// 0x000 64 arg[0]: escrow id
|
||||
// 0x040 128 arg[1]: seller address or winner string
|
||||
// 0x140 128 caller buffer
|
||||
// 0x1C0 64 treasury buffer
|
||||
// 0x200 128 state-read buffer 1 (buyer or seller)
|
||||
// 0x280 128 state-read buffer 2 (seller or admin)
|
||||
// 0x300 2 status byte buffer
|
||||
// 0x310 256 scratch (buildField key workspace)
|
||||
//
|
||||
// Constants at 0x500+:
|
||||
// 0x500 5 "admin"
|
||||
// 0x506 13 "initialized: "
|
||||
// 0x514 10 "created: "
|
||||
// 0x51E 10 "released: "
|
||||
// 0x529 10 "refunded: "
|
||||
// 0x534 10 "disputed: "
|
||||
// 0x53F 10 "resolved: "
|
||||
// 0x54A 14 "unauthorized: "
|
||||
// 0x559 11 "not found: "
|
||||
// 0x565 14 "already init: "
|
||||
// 0x574 12 "not active: "
|
||||
// 0x581 14 "not disputed: "
|
||||
// 0x590 7 "buyer: "
|
||||
// 0x598 8 "seller: "
|
||||
// 0x5A1 8 "status: "
|
||||
// 0x5AA 7 "active"
|
||||
// 0x5B1 9 "disputed"
|
||||
// 0x5BB 9 "released"
|
||||
// 0x5C5 8 "refunded"
|
||||
// 0x5CE 6 "buyer" (for resolve winner comparison)
|
||||
// 0x5D4 6 "seller"
|
||||
|
||||
const (
|
||||
offArg0 int32 = 0x000
|
||||
offArg1 int32 = 0x040
|
||||
offCaller int32 = 0x140
|
||||
offTreasury int32 = 0x1C0
|
||||
offRead1 int32 = 0x200
|
||||
offRead2 int32 = 0x280
|
||||
offStatBuf int32 = 0x300
|
||||
offScratch int32 = 0x310
|
||||
|
||||
offKeyAdmin int32 = 0x500
|
||||
offInitedPfx int32 = 0x506
|
||||
offCreatedPfx int32 = 0x514
|
||||
offReleasedPfx int32 = 0x51E
|
||||
offRefundedPfx int32 = 0x529
|
||||
offDisputedPfx int32 = 0x534
|
||||
offResolvedPfx int32 = 0x53F
|
||||
offUnauthPfx int32 = 0x54A
|
||||
offNotFoundPfx int32 = 0x559
|
||||
offAlreadyPfx int32 = 0x565
|
||||
offNotActivePfx int32 = 0x574
|
||||
offNotDispPfx int32 = 0x581
|
||||
offBuyerPfx int32 = 0x590
|
||||
offSellerPfx int32 = 0x598
|
||||
offStatusPfx int32 = 0x5A1
|
||||
offStrActive int32 = 0x5AA
|
||||
offStrDisputed int32 = 0x5B1
|
||||
offStrReleased int32 = 0x5BB
|
||||
offStrRefunded int32 = 0x5C5
|
||||
offStrBuyer int32 = 0x5CE
|
||||
offStrSeller int32 = 0x5D4
|
||||
)
|
||||
|
||||
// ── Import / function indices ─────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
fnGetArgStr = 0
|
||||
fnGetArgU64 = 1
|
||||
fnGetCaller = 2
|
||||
fnGetState = 3
|
||||
fnSetState = 4
|
||||
fnLog = 5
|
||||
fnTransfer = 6
|
||||
fnGetContractTreasury = 7
|
||||
fnPutU64 = 8
|
||||
fnGetU64 = 9
|
||||
|
||||
fnBytesEqual = 10
|
||||
fnMemcpy = 11
|
||||
fnLogPrefix = 12
|
||||
fnBuildField = 13 // (idOff, idLen, fieldChar i32) → keyLen i32
|
||||
fnInit = 14
|
||||
fnCreate = 15
|
||||
fnRelease = 16
|
||||
fnRefund = 17
|
||||
fnDispute = 18
|
||||
fnResolve = 19
|
||||
fnInfo = 20
|
||||
)
|
||||
|
||||
// ── Helper bodies ─────────────────────────────────────────────────────────────
|
||||
|
||||
func bytesEqualBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(2, tI32)),
|
||||
ic32(1), lset(4), ic32(0), lset(3),
|
||||
block_(), loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1),
|
||||
lget(0), lget(3), i32Add(), i32Load8U(),
|
||||
lget(1), lget(3), i32Add(), i32Load8U(),
|
||||
i32Ne(), if_(), ic32(0), lset(4), br_(2), end_(),
|
||||
lget(3), ic32(1), i32Add(), lset(3),
|
||||
br_(0), end_(), end_(),
|
||||
lget(4),
|
||||
)
|
||||
}
|
||||
|
||||
func memcpyBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(1, tI32)),
|
||||
ic32(0), lset(3),
|
||||
block_(), loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1),
|
||||
lget(0), lget(3), i32Add(),
|
||||
lget(1), lget(3), i32Add(), i32Load8U(),
|
||||
i32Store8(),
|
||||
lget(3), ic32(1), i32Add(), lset(3),
|
||||
br_(0), end_(), end_(),
|
||||
)
|
||||
}
|
||||
|
||||
func logPrefixBody() []byte {
|
||||
// (prefixPtr, prefixLen, suffixPtr, suffixLen i32)
|
||||
return funcBody(
|
||||
noLocals,
|
||||
ic32(offScratch), lget(0), lget(1), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), i32Add(), lget(2), lget(3), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), lget(3), i32Add(), call(fnLog),
|
||||
)
|
||||
}
|
||||
|
||||
// buildField(idOff, idLen, fieldChar i32) → keyLen i32
|
||||
// Writes "e:<id>:<fieldChar>" into offScratch.
|
||||
func buildFieldBody() []byte {
|
||||
return funcBody(
|
||||
noLocals,
|
||||
ic32(offScratch), ic32('e'), i32Store8(),
|
||||
ic32(offScratch+1), ic32(':'), i32Store8(),
|
||||
ic32(offScratch+2), lget(0), lget(1), call(fnMemcpy),
|
||||
ic32(offScratch+2), lget(1), i32Add(), ic32(':'), i32Store8(),
|
||||
ic32(offScratch+3), lget(1), i32Add(), lget(2), i32Store8(),
|
||||
lget(1), ic32(4), i32Add(),
|
||||
)
|
||||
}
|
||||
|
||||
// isCallerAdminCode: reads admin into offRead2, checks caller == admin.
|
||||
// Params: callerLen local index, adminLen local index.
|
||||
// Returns inline code that leaves i32 (1=is admin) on stack.
|
||||
func isCallerAdminCode(callerLenLocal, adminLenLocal uint32) []byte {
|
||||
return cat(
|
||||
ic32(offKeyAdmin), ic32(5), ic32(offRead2), ic32(128), call(fnGetState), lset(adminLenLocal),
|
||||
lget(adminLenLocal), i32Eqz(),
|
||||
ifI32(), ic32(0), else_(),
|
||||
lget(callerLenLocal), lget(adminLenLocal), i32Ne(),
|
||||
ifI32(), ic32(0), else_(),
|
||||
ic32(offCaller), ic32(offRead2), lget(callerLenLocal), call(fnBytesEqual),
|
||||
end_(), end_(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Contract method bodies ────────────────────────────────────────────────────
|
||||
|
||||
// init() — set caller as admin
|
||||
// Locals: callerLen(0), existingLen(1)
|
||||
func initBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(2, tI32)),
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(0),
|
||||
ic32(offKeyAdmin), ic32(5), ic32(offRead2), ic32(128), call(fnGetState), lset(1),
|
||||
lget(1), ic32(0), i32GtU(), if_(),
|
||||
ic32(offAlreadyPfx), ic32(14), ic32(offCaller), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offKeyAdmin), ic32(5), ic32(offCaller), lget(0), call(fnSetState),
|
||||
ic32(offInitedPfx), ic32(13), ic32(offCaller), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// create(id, seller, amount u64)
|
||||
// Locals: idLen(0), sellerLen(1), callerLen(2), treasuryLen(3), keyLen(4) [i32]
|
||||
// amount(5) [i64]
|
||||
func createBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(5, tI32), localDecl(1, tI64)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
ic32(1), ic32(offArg1), ic32(128), call(fnGetArgStr), lset(1),
|
||||
lget(1), i32Eqz(), if_(), return_(), end_(),
|
||||
ic32(2), call(fnGetArgU64), lset(5),
|
||||
|
||||
// Check id not already used
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offStatBuf), ic32(2), call(fnGetState),
|
||||
ic32(0), i32GtU(), if_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get caller (buyer) and treasury
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(2),
|
||||
ic32(offTreasury), ic32(64), call(fnGetContractTreasury), lset(3),
|
||||
|
||||
// Transfer amount: buyer → treasury
|
||||
ic32(offCaller), lget(2), ic32(offTreasury), lget(3), lget(5), call(fnTransfer), drop(),
|
||||
|
||||
// Write buyer: e:<id>:b → caller
|
||||
ic32(offArg0), lget(0), ic32('b'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offCaller), lget(2), call(fnSetState),
|
||||
|
||||
// Write seller: e:<id>:s → arg1
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offArg1), lget(1), call(fnSetState),
|
||||
|
||||
// Write amount: e:<id>:v
|
||||
ic32(offArg0), lget(0), ic32('v'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), lget(5), call(fnPutU64),
|
||||
|
||||
// Write status = 'a': e:<id>:x
|
||||
ic32(offStatBuf), ic32('a'), i32Store8(),
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offStatBuf), ic32(1), call(fnSetState),
|
||||
|
||||
ic32(offCreatedPfx), ic32(9), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// checkActiveStatus: reads status byte into offStatBuf; returns inline code
|
||||
// that returns with an error log if status != expectedChar.
|
||||
func checkStatus(idLenLocal uint32, expectedChar int32, errPfxOff int32, errPfxLen int32) []byte {
|
||||
return cat(
|
||||
ic32(offArg0), lget(idLenLocal), ic32('x'), call(fnBuildField),
|
||||
// keyLen on stack — but we need it in a local. Use the call result directly:
|
||||
// Actually we need to save it. This is getting complex. Let me inline differently.
|
||||
// Instead, we'll read status without saving keyLen — just check immediately.
|
||||
// Actually the fn returns keyLen on stack. We need (keyPtr, keyLen, dstPtr, dstLen) for get_state.
|
||||
// So: ic32(offScratch), [keyLen], ic32(offStatBuf), ic32(2), call(fnGetState)
|
||||
// But keyLen is already on the stack after buildField!
|
||||
// We can do: ic32(offScratch), <stack: keyLen>, ...
|
||||
// No wait, stack ordering: we push ic32(offScratch) BEFORE the keyLen.
|
||||
// For get_state(keyPtr, keyLen, dstPtr, dstLen):
|
||||
// We need: keyPtr first, then keyLen.
|
||||
// buildField leaves keyLen on stack, but we need keyPtr first.
|
||||
// This is the fundamental issue: the keyPtr (offScratch) needs to be pushed before keyLen.
|
||||
//
|
||||
// Fix: save keyLen to a local first, then push offScratch, keyLen.
|
||||
// But we don't have a spare local in this helper...
|
||||
//
|
||||
// For now, let me just not use this helper function and inline the check in each method.
|
||||
ic32(0), // placeholder - don't use this helper
|
||||
)
|
||||
}
|
||||
|
||||
// release(id) — buyer releases funds to seller
|
||||
// Locals: idLen(0), buyerLen(1), sellerLen(2), callerLen(3), treasuryLen(4), keyLen(5) [i32]
|
||||
// amount(6) [i64]
|
||||
func releaseBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(6, tI32), localDecl(1, tI64)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Check status == 'a'
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), ic32(offStatBuf), ic32(2), call(fnGetState),
|
||||
ic32(0), i32Ne(), if_(),
|
||||
ic32(offStatBuf), i32Load8U(), ic32('a'), i32Ne(), if_(),
|
||||
ic32(offNotActivePfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
else_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Check caller == buyer
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(3),
|
||||
ic32(offArg0), lget(0), ic32('b'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), ic32(offRead1), ic32(128), call(fnGetState), lset(1),
|
||||
lget(3), lget(1), i32Ne(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offCaller), ic32(offRead1), lget(3), call(fnBytesEqual),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get seller, treasury, amount
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), ic32(offRead2), ic32(128), call(fnGetState), lset(2),
|
||||
ic32(offTreasury), ic32(64), call(fnGetContractTreasury), lset(4),
|
||||
ic32(offArg0), lget(0), ic32('v'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), call(fnGetU64), lset(6),
|
||||
|
||||
// Transfer: treasury → seller
|
||||
ic32(offTreasury), lget(4), ic32(offRead2), lget(2), lget(6), call(fnTransfer), drop(),
|
||||
|
||||
// Mark status = 'r'
|
||||
ic32(offStatBuf), ic32('r'), i32Store8(),
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), ic32(offStatBuf), ic32(1), call(fnSetState),
|
||||
|
||||
ic32(offReleasedPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// refund(id) — seller voluntarily refunds buyer
|
||||
// Locals: idLen(0), sellerLen(1), buyerLen(2), callerLen(3), treasuryLen(4), keyLen(5) [i32]
|
||||
// amount(6) [i64]
|
||||
func refundBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(6, tI32), localDecl(1, tI64)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Check status == 'a'
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), ic32(offStatBuf), ic32(2), call(fnGetState),
|
||||
ic32(0), i32Ne(), if_(),
|
||||
ic32(offStatBuf), i32Load8U(), ic32('a'), i32Ne(), if_(),
|
||||
ic32(offNotActivePfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
else_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Check caller == seller
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(3),
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), ic32(offRead1), ic32(128), call(fnGetState), lset(1),
|
||||
lget(3), lget(1), i32Ne(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offCaller), ic32(offRead1), lget(3), call(fnBytesEqual),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get buyer, treasury, amount
|
||||
ic32(offArg0), lget(0), ic32('b'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), ic32(offRead2), ic32(128), call(fnGetState), lset(2),
|
||||
ic32(offTreasury), ic32(64), call(fnGetContractTreasury), lset(4),
|
||||
ic32(offArg0), lget(0), ic32('v'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), call(fnGetU64), lset(6),
|
||||
|
||||
// Transfer: treasury → buyer
|
||||
ic32(offTreasury), lget(4), ic32(offRead2), lget(2), lget(6), call(fnTransfer), drop(),
|
||||
|
||||
// Mark status = 'f'
|
||||
ic32(offStatBuf), ic32('f'), i32Store8(),
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(5),
|
||||
ic32(offScratch), lget(5), ic32(offStatBuf), ic32(1), call(fnSetState),
|
||||
|
||||
ic32(offRefundedPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// dispute(id) — buyer or seller raises a dispute
|
||||
// Locals: idLen(0), callerLen(1), buyerLen(2), sellerLen(3), keyLen(4) [i32]
|
||||
func disputeBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(5, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Check status == 'a'
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offStatBuf), ic32(2), call(fnGetState),
|
||||
ic32(0), i32Ne(), if_(),
|
||||
ic32(offStatBuf), i32Load8U(), ic32('a'), i32Ne(), if_(),
|
||||
ic32(offNotActivePfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
else_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Caller must be buyer or seller
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
ic32(offArg0), lget(0), ic32('b'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offRead1), ic32(128), call(fnGetState), lset(2),
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offRead2), ic32(128), call(fnGetState), lset(3),
|
||||
|
||||
// isBuyer = callerLen==buyerLen && bytes_equal(caller, buyer, callerLen)
|
||||
// isSeller = callerLen==sellerLen && bytes_equal(caller, seller, callerLen)
|
||||
// if !isBuyer && !isSeller: unauthorized
|
||||
lget(1), lget(2), i32Ne(),
|
||||
if_(),
|
||||
// callerLen != buyerLen → check seller
|
||||
lget(1), lget(3), i32Ne(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offCaller), ic32(offRead2), lget(1), call(fnBytesEqual),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
else_(),
|
||||
// callerLen == buyerLen → check bytes
|
||||
ic32(offCaller), ic32(offRead1), lget(1), call(fnBytesEqual),
|
||||
i32Eqz(), if_(),
|
||||
// not buyer — check seller
|
||||
lget(1), lget(3), i32Ne(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offCaller), ic32(offRead2), lget(1), call(fnBytesEqual),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
end_(),
|
||||
end_(),
|
||||
|
||||
// Mark status = 'd'
|
||||
ic32(offStatBuf), ic32('d'), i32Store8(),
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offStatBuf), ic32(1), call(fnSetState),
|
||||
ic32(offDisputedPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// resolve(id, winner) — admin resolves disputed escrow
|
||||
// winner arg must be "buyer" or "seller"
|
||||
// Locals: idLen(0), winnerLen(1), callerLen(2), adminLen(3),
|
||||
// recipientLen(4), treasuryLen(5), keyLen(6) [i32]
|
||||
// amount(7) [i64]
|
||||
func resolveBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(7, tI32), localDecl(1, tI64)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
ic32(1), ic32(offArg1), ic32(128), call(fnGetArgStr), lset(1),
|
||||
lget(1), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Check status == 'd'
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(6),
|
||||
ic32(offScratch), lget(6), ic32(offStatBuf), ic32(2), call(fnGetState),
|
||||
ic32(0), i32Ne(), if_(),
|
||||
ic32(offStatBuf), i32Load8U(), ic32('d'), i32Ne(), if_(),
|
||||
ic32(offNotDispPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
else_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Check caller is admin
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(2),
|
||||
isCallerAdminCode(2, 3),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Determine recipient based on winner arg
|
||||
// winner == "buyer" (5 bytes) → recipient = buyer address
|
||||
// winner == "seller" (6 bytes) → recipient = seller address
|
||||
ic32(offTreasury), ic32(64), call(fnGetContractTreasury), lset(5),
|
||||
ic32(offArg0), lget(0), ic32('v'), call(fnBuildField), lset(6),
|
||||
ic32(offScratch), lget(6), call(fnGetU64), lset(7),
|
||||
|
||||
// Compare winner to "buyer"
|
||||
lget(1), ic32(5), i32Ne(), if_(),
|
||||
// not "buyer" — assume "seller"
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(6),
|
||||
ic32(offScratch), lget(6), ic32(offRead1), ic32(128), call(fnGetState), lset(4),
|
||||
else_(),
|
||||
// might be "buyer" — verify bytes
|
||||
ic32(offArg1), ic32(offStrBuyer), ic32(5), call(fnBytesEqual),
|
||||
i32Eqz(), if_(),
|
||||
// not "buyer" bytes — default to seller
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(6),
|
||||
ic32(offScratch), lget(6), ic32(offRead1), ic32(128), call(fnGetState), lset(4),
|
||||
else_(),
|
||||
ic32(offArg0), lget(0), ic32('b'), call(fnBuildField), lset(6),
|
||||
ic32(offScratch), lget(6), ic32(offRead1), ic32(128), call(fnGetState), lset(4),
|
||||
end_(),
|
||||
end_(),
|
||||
|
||||
// Transfer: treasury → recipient
|
||||
ic32(offTreasury), lget(5), ic32(offRead1), lget(4), lget(7), call(fnTransfer), drop(),
|
||||
|
||||
// Mark status = 'r' (released, settled)
|
||||
ic32(offStatBuf), ic32('r'), i32Store8(),
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(6),
|
||||
ic32(offScratch), lget(6), ic32(offStatBuf), ic32(1), call(fnSetState),
|
||||
|
||||
ic32(offResolvedPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
)
|
||||
}
|
||||
|
||||
// info(id) — log escrow details
|
||||
// Locals: idLen(0), buyerLen(1), sellerLen(2), keyLen(3) [i32]
|
||||
func infoBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(4, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Check exists
|
||||
ic32(offArg0), lget(0), ic32('x'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offStatBuf), ic32(2), call(fnGetState),
|
||||
ic32(0), i32Ne(), i32Eqz(), if_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefix),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Log buyer
|
||||
ic32(offArg0), lget(0), ic32('b'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offRead1), ic32(128), call(fnGetState), lset(1),
|
||||
ic32(offBuyerPfx), ic32(7), ic32(offRead1), lget(1), call(fnLogPrefix),
|
||||
|
||||
// Log seller
|
||||
ic32(offArg0), lget(0), ic32('s'), call(fnBuildField), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offRead2), ic32(128), call(fnGetState), lset(2),
|
||||
ic32(offSellerPfx), ic32(8), ic32(offRead2), lget(2), call(fnLogPrefix),
|
||||
|
||||
// Log status
|
||||
ic32(offStatBuf), i32Load8U(), ic32('a'), i32Ne(), if_(),
|
||||
ic32(offStatBuf), i32Load8U(), ic32('d'), i32Ne(), if_(),
|
||||
ic32(offStatBuf), i32Load8U(), ic32('r'), i32Ne(), if_(),
|
||||
ic32(offStatusPfx), ic32(8), ic32(offStrRefunded), ic32(8), call(fnLogPrefix),
|
||||
else_(),
|
||||
ic32(offStatusPfx), ic32(8), ic32(offStrReleased), ic32(8), call(fnLogPrefix),
|
||||
end_(),
|
||||
else_(),
|
||||
ic32(offStatusPfx), ic32(8), ic32(offStrDisputed), ic32(8), call(fnLogPrefix),
|
||||
end_(),
|
||||
else_(),
|
||||
ic32(offStatusPfx), ic32(8), ic32(offStrActive), ic32(6), call(fnLogPrefix),
|
||||
end_(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func main() {
|
||||
// Types:
|
||||
// 0: (i32,i32,i32)→(i32) get_arg_str, bytes_equal, buildField
|
||||
// 1: (i32)→(i64) get_arg_u64
|
||||
// 2: (i32,i32,i32,i32)→(i32) get_state
|
||||
// 3: (i32,i32,i32,i32)→() set_state, log_prefix
|
||||
// 4: (i32,i32)→() log
|
||||
// 5: (i32,i32,i32,i32,i64)→(i32) transfer
|
||||
// 6: (i32,i32)→(i32) get_caller, get_contract_treasury
|
||||
// 7: (i32,i32,i64)→() put_u64
|
||||
// 8: (i32,i32)→(i64) get_u64
|
||||
// 9: ()→() exported methods
|
||||
// 10: (i32,i32,i32)→() memcpy
|
||||
typeSection := section(0x01, vec(
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{tI32}), // 0
|
||||
functype([]byte{tI32}, []byte{tI64}), // 1
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{tI32}), // 2
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{}), // 3
|
||||
functype([]byte{tI32, tI32}, []byte{}), // 4
|
||||
functype([]byte{tI32, tI32, tI32, tI32, tI64}, []byte{tI32}), // 5
|
||||
functype([]byte{tI32, tI32}, []byte{tI32}), // 6
|
||||
functype([]byte{tI32, tI32, tI64}, []byte{}), // 7
|
||||
functype([]byte{tI32, tI32}, []byte{tI64}), // 8
|
||||
functype([]byte{}, []byte{}), // 9
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{}), // 10
|
||||
))
|
||||
|
||||
importSection := section(0x02, vec(
|
||||
importFunc("env", "get_arg_str", 0), // 0
|
||||
importFunc("env", "get_arg_u64", 1), // 1
|
||||
importFunc("env", "get_caller", 6), // 2
|
||||
importFunc("env", "get_state", 2), // 3
|
||||
importFunc("env", "set_state", 3), // 4
|
||||
importFunc("env", "log", 4), // 5
|
||||
importFunc("env", "transfer", 5), // 6
|
||||
importFunc("env", "get_contract_treasury", 6), // 7
|
||||
importFunc("env", "put_u64", 7), // 8
|
||||
importFunc("env", "get_u64", 8), // 9
|
||||
))
|
||||
|
||||
// 11 local functions
|
||||
functionSection := section(0x03, vec(
|
||||
u(0), // bytes_equal type 0
|
||||
u(10), // memcpy type 10
|
||||
u(3), // log_prefix type 3
|
||||
u(0), // build_field type 0
|
||||
u(9), // init type 9
|
||||
u(9), // create type 9
|
||||
u(9), // release type 9
|
||||
u(9), // refund type 9
|
||||
u(9), // dispute type 9
|
||||
u(9), // resolve type 9
|
||||
u(9), // info type 9
|
||||
))
|
||||
|
||||
memorySection := section(0x05, vec(cat([]byte{0x00}, u(2)))) // 2 pages
|
||||
|
||||
exportSection := section(0x07, vec(
|
||||
exportEntry("memory", 0x02, 0),
|
||||
exportEntry("init", 0x00, fnInit),
|
||||
exportEntry("create", 0x00, fnCreate),
|
||||
exportEntry("release", 0x00, fnRelease),
|
||||
exportEntry("refund", 0x00, fnRefund),
|
||||
exportEntry("dispute", 0x00, fnDispute),
|
||||
exportEntry("resolve", 0x00, fnResolve),
|
||||
exportEntry("info", 0x00, fnInfo),
|
||||
))
|
||||
|
||||
dataSection := section(0x0B, cat(
|
||||
u(21),
|
||||
dataSegment(offKeyAdmin, []byte("admin")),
|
||||
dataSegment(offInitedPfx, []byte("initialized: ")),
|
||||
dataSegment(offCreatedPfx, []byte("created: ")),
|
||||
dataSegment(offReleasedPfx, []byte("released: ")),
|
||||
dataSegment(offRefundedPfx, []byte("refunded: ")),
|
||||
dataSegment(offDisputedPfx, []byte("disputed: ")),
|
||||
dataSegment(offResolvedPfx, []byte("resolved: ")),
|
||||
dataSegment(offUnauthPfx, []byte("unauthorized: ")),
|
||||
dataSegment(offNotFoundPfx, []byte("not found: ")),
|
||||
dataSegment(offAlreadyPfx, []byte("already init: ")),
|
||||
dataSegment(offNotActivePfx,[]byte("not active: ")),
|
||||
dataSegment(offNotDispPfx, []byte("not disputed: ")),
|
||||
dataSegment(offBuyerPfx, []byte("buyer: ")),
|
||||
dataSegment(offSellerPfx, []byte("seller: ")),
|
||||
dataSegment(offStatusPfx, []byte("status: ")),
|
||||
dataSegment(offStrActive, []byte("active")),
|
||||
dataSegment(offStrDisputed, []byte("disputed")),
|
||||
dataSegment(offStrReleased, []byte("released")),
|
||||
dataSegment(offStrRefunded, []byte("refunded")),
|
||||
dataSegment(offStrBuyer, []byte("buyer")),
|
||||
dataSegment(offStrSeller, []byte("seller")),
|
||||
))
|
||||
|
||||
codeSection := section(0x0A, cat(
|
||||
u(11),
|
||||
bytesEqualBody(),
|
||||
memcpyBody(),
|
||||
logPrefixBody(),
|
||||
buildFieldBody(),
|
||||
initBody(),
|
||||
createBody(),
|
||||
releaseBody(),
|
||||
refundBody(),
|
||||
disputeBody(),
|
||||
resolveBody(),
|
||||
infoBody(),
|
||||
))
|
||||
|
||||
module := cat(
|
||||
[]byte{0x00, 0x61, 0x73, 0x6d},
|
||||
[]byte{0x01, 0x00, 0x00, 0x00},
|
||||
typeSection,
|
||||
importSection,
|
||||
functionSection,
|
||||
memorySection,
|
||||
exportSection,
|
||||
dataSection,
|
||||
codeSection,
|
||||
)
|
||||
|
||||
out := "contracts/escrow/escrow.wasm"
|
||||
if err := os.WriteFile(out, module, 0644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "write:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Written %s (%d bytes)\n", out, len(module))
|
||||
}
|
||||
538
contracts/governance/gen/main.go
Normal file
538
contracts/governance/gen/main.go
Normal file
@@ -0,0 +1,538 @@
|
||||
// gen generates contracts/governance/governance.wasm
|
||||
// Run from repo root: go run ./contracts/governance/gen/
|
||||
//
|
||||
// Methods: init, propose, approve, reject, get, get_pending, set_admin
|
||||
//
|
||||
// State layout (all keys are raw UTF-8 strings):
|
||||
// "admin" → admin address bytes
|
||||
// "param:<key>" → current live value bytes
|
||||
// "prop:<key>" → pending proposed value bytes
|
||||
//
|
||||
// Access control:
|
||||
// approve / reject / set_admin → admin only
|
||||
// propose / get / get_pending → anyone
|
||||
// init → anyone (but only writes if admin not set)
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ── LEB128 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func u(v uint64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
if v != 0 {
|
||||
bt |= 0x80
|
||||
}
|
||||
b = append(b, bt)
|
||||
if v == 0 {
|
||||
return b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func s(v int64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
sign := (bt & 0x40) != 0
|
||||
if (v == 0 && !sign) || (v == -1 && sign) {
|
||||
return append(b, bt)
|
||||
}
|
||||
b = append(b, bt|0x80)
|
||||
}
|
||||
}
|
||||
|
||||
func cat(slices ...[]byte) []byte {
|
||||
var out []byte
|
||||
for _, sl := range slices {
|
||||
out = append(out, sl...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func wstr(str string) []byte { return cat(u(uint64(len(str))), []byte(str)) }
|
||||
|
||||
func section(id byte, content []byte) []byte {
|
||||
return cat([]byte{id}, u(uint64(len(content))), content)
|
||||
}
|
||||
|
||||
func vec(items ...[]byte) []byte {
|
||||
out := u(uint64(len(items)))
|
||||
for _, it := range items {
|
||||
out = append(out, it...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func functype(params, results []byte) []byte {
|
||||
return cat([]byte{0x60}, u(uint64(len(params))), params, u(uint64(len(results))), results)
|
||||
}
|
||||
|
||||
func importFunc(mod, name string, typeIdx uint32) []byte {
|
||||
return cat(wstr(mod), wstr(name), []byte{0x00}, u(uint64(typeIdx)))
|
||||
}
|
||||
|
||||
func exportEntry(name string, kind byte, idx uint32) []byte {
|
||||
return cat(wstr(name), []byte{kind}, u(uint64(idx)))
|
||||
}
|
||||
|
||||
func dataSegment(offset int32, data []byte) []byte {
|
||||
return cat(
|
||||
[]byte{0x00},
|
||||
[]byte{0x41}, s(int64(offset)), []byte{0x0B},
|
||||
u(uint64(len(data))), data,
|
||||
)
|
||||
}
|
||||
|
||||
func funcBody(localDecls []byte, instrs ...[]byte) []byte {
|
||||
inner := cat(localDecls)
|
||||
for _, ins := range instrs {
|
||||
inner = append(inner, ins...)
|
||||
}
|
||||
inner = append(inner, 0x0B)
|
||||
return cat(u(uint64(len(inner))), inner)
|
||||
}
|
||||
|
||||
var noLocals = u(0)
|
||||
|
||||
func localDecl(n uint32, typ byte) []byte { return cat(u(uint64(n)), []byte{typ}) }
|
||||
func withLocals(decls ...[]byte) []byte {
|
||||
return cat(u(uint64(len(decls))), cat(decls...))
|
||||
}
|
||||
|
||||
const (
|
||||
tI32 byte = 0x7F
|
||||
tI64 byte = 0x7E
|
||||
)
|
||||
|
||||
func call(fn uint32) []byte { return cat([]byte{0x10}, u(uint64(fn))) }
|
||||
func lget(i uint32) []byte { return cat([]byte{0x20}, u(uint64(i))) }
|
||||
func lset(i uint32) []byte { return cat([]byte{0x21}, u(uint64(i))) }
|
||||
func ic32(v int32) []byte { return cat([]byte{0x41}, s(int64(v))) }
|
||||
func block_() []byte { return []byte{0x02, 0x40} }
|
||||
func loop_() []byte { return []byte{0x03, 0x40} }
|
||||
func if_() []byte { return []byte{0x04, 0x40} }
|
||||
func ifI32() []byte { return []byte{0x04, tI32} }
|
||||
func else_() []byte { return []byte{0x05} }
|
||||
func end_() []byte { return []byte{0x0B} }
|
||||
func br_(lbl uint32) []byte { return cat([]byte{0x0C}, u(uint64(lbl))) }
|
||||
func brIf_(lbl uint32) []byte { return cat([]byte{0x0D}, u(uint64(lbl))) }
|
||||
func return_() []byte { return []byte{0x0F} }
|
||||
func i32Eqz() []byte { return []byte{0x45} }
|
||||
func i32Ne() []byte { return []byte{0x47} }
|
||||
func i32GtU() []byte { return []byte{0x4B} }
|
||||
func i32GeU() []byte { return []byte{0x4F} }
|
||||
func i32Add() []byte { return []byte{0x6A} }
|
||||
func i32Load8U() []byte { return []byte{0x2D, 0x00, 0x00} }
|
||||
func i32Store8() []byte { return []byte{0x3A, 0x00, 0x00} }
|
||||
|
||||
// ── Memory layout ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// 0x000 64 arg[0] key buffer
|
||||
// 0x040 256 arg[1] value buffer
|
||||
// 0x140 128 caller buffer
|
||||
// 0x1C0 128 admin state-read buffer
|
||||
// 0x240 256 value state-read buffer
|
||||
// 0x340 256 scratch (build_key writes here, log_prefix_name writes here)
|
||||
//
|
||||
// Constant strings:
|
||||
// 0x500 6 "admin" (state key)
|
||||
// 0x506 6 "param:" (state key prefix)
|
||||
// 0x50D 5 "prop:" (state key prefix)
|
||||
// 0x513 13 "initialized: "
|
||||
// 0x521 9 "proposed: "
|
||||
// 0x52B 10 "approved: "
|
||||
// 0x536 10 "rejected: "
|
||||
// 0x541 8 "value: "
|
||||
// 0x549 11 "not set: "
|
||||
// 0x555 9 "pending: "
|
||||
// 0x55F 12 "no pending: "
|
||||
// 0x56C 7 "admin: "
|
||||
// 0x574 14 "unauthorized: "
|
||||
// 0x583 14 "already init: "
|
||||
|
||||
const (
|
||||
offArg0 int32 = 0x000
|
||||
offArg1 int32 = 0x040
|
||||
offCaller int32 = 0x140
|
||||
offAdminRead int32 = 0x1C0
|
||||
offValRead int32 = 0x240
|
||||
offScratch int32 = 0x340
|
||||
|
||||
offKeyAdmin int32 = 0x500 // "admin" 5 bytes
|
||||
offPfxParam int32 = 0x506 // "param:" 6 bytes
|
||||
offPfxProp int32 = 0x50D // "prop:" 5 bytes
|
||||
offInitedPfx int32 = 0x513 // "initialized: " 14
|
||||
offProposedPfx int32 = 0x521 // "proposed: " 9 (key=val shown as arg0=arg1)
|
||||
offApprovedPfx int32 = 0x52B // "approved: " 10
|
||||
offRejectedPfx int32 = 0x536 // "rejected: " 10
|
||||
offValuePfx int32 = 0x541 // "value: " 7
|
||||
offNotSetPfx int32 = 0x549 // "not set: " 9
|
||||
offPendingPfx int32 = 0x555 // "pending: " 9
|
||||
offNoPendingPfx int32 = 0x55F // "no pending: " 12
|
||||
offAdminPfx int32 = 0x56C // "admin: " 7
|
||||
offUnauthPfx int32 = 0x574 // "unauthorized: " 14
|
||||
offAlreadyPfx int32 = 0x583 // "already init" 12
|
||||
)
|
||||
|
||||
const (
|
||||
fnGetArgStr = 0
|
||||
fnGetCaller = 1
|
||||
fnGetState = 2
|
||||
fnSetState = 3
|
||||
fnLog = 4
|
||||
|
||||
fnBytesEqual = 5
|
||||
fnMemcpy = 6
|
||||
fnLogPrefixName = 7
|
||||
fnBuildKey = 8
|
||||
fnInit = 9
|
||||
fnPropose = 10
|
||||
fnApprove = 11
|
||||
fnReject = 12
|
||||
fnGet = 13
|
||||
fnGetPending = 14
|
||||
fnSetAdmin = 15
|
||||
)
|
||||
|
||||
// ── Helper bodies ─────────────────────────────────────────────────────────────
|
||||
|
||||
func bytesEqualBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(2, tI32)),
|
||||
ic32(1), lset(4), ic32(0), lset(3),
|
||||
block_(), loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1),
|
||||
lget(0), lget(3), i32Add(), i32Load8U(),
|
||||
lget(1), lget(3), i32Add(), i32Load8U(),
|
||||
i32Ne(), if_(), ic32(0), lset(4), br_(2), end_(),
|
||||
lget(3), ic32(1), i32Add(), lset(3),
|
||||
br_(0), end_(), end_(),
|
||||
lget(4),
|
||||
)
|
||||
}
|
||||
|
||||
func memcpyBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(1, tI32)),
|
||||
ic32(0), lset(3),
|
||||
block_(), loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1),
|
||||
lget(0), lget(3), i32Add(),
|
||||
lget(1), lget(3), i32Add(), i32Load8U(),
|
||||
i32Store8(),
|
||||
lget(3), ic32(1), i32Add(), lset(3),
|
||||
br_(0), end_(), end_(),
|
||||
)
|
||||
}
|
||||
|
||||
func logPrefixNameBody() []byte {
|
||||
return funcBody(
|
||||
noLocals,
|
||||
ic32(offScratch), lget(0), lget(1), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), i32Add(), lget(2), lget(3), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), lget(3), i32Add(), call(fnLog),
|
||||
)
|
||||
}
|
||||
|
||||
// $build_key(pfxOff, pfxLen, dataOff, dataLen i32) → keyLen i32
|
||||
func buildKeyBody() []byte {
|
||||
return funcBody(
|
||||
noLocals,
|
||||
ic32(offScratch), lget(0), lget(1), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), i32Add(), lget(2), lget(3), call(fnMemcpy),
|
||||
lget(1), lget(3), i32Add(),
|
||||
)
|
||||
}
|
||||
|
||||
// isCallerAdmin: reads admin from state into offAdminRead, returns 1 if caller==admin.
|
||||
// (callerLenLocal i32) → i32
|
||||
// Leaves adminLen on stack as side effect (stored in local adminLenLocal).
|
||||
// Emits code that: reads admin, compares with caller.
|
||||
// Uses offAdminRead for the state read buffer.
|
||||
func isCallerAdminCode(callerLenLocal, adminLenLocal uint32) []byte {
|
||||
return cat(
|
||||
// adminLen = get_state("admin", 5, offAdminRead, 128)
|
||||
ic32(offKeyAdmin), ic32(5), ic32(offAdminRead), ic32(128), call(fnGetState), lset(adminLenLocal),
|
||||
// if adminLen == 0: not initialized → return 0
|
||||
lget(adminLenLocal), i32Eqz(),
|
||||
ifI32(),
|
||||
ic32(0),
|
||||
else_(),
|
||||
// bytes_equal(offCaller, offAdminRead, callerLen) if callerLen==adminLen else 0
|
||||
lget(callerLenLocal), lget(adminLenLocal), i32Ne(),
|
||||
ifI32(), ic32(0), else_(),
|
||||
ic32(offCaller), ic32(offAdminRead), lget(callerLenLocal), call(fnBytesEqual),
|
||||
end_(),
|
||||
end_(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Contract method bodies ────────────────────────────────────────────────────
|
||||
|
||||
// init() — set caller as admin (only if not already initialized)
|
||||
func initBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(2, tI32)),
|
||||
// callerLen = get_caller(offCaller, 128)
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(0),
|
||||
// Check if admin already set
|
||||
ic32(offKeyAdmin), ic32(5), ic32(offAdminRead), ic32(128), call(fnGetState), lset(1),
|
||||
lget(1), ic32(0), i32GtU(), if_(),
|
||||
ic32(offAlreadyPfx), ic32(12), ic32(offCaller), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
// set state["admin"] = caller
|
||||
ic32(offKeyAdmin), ic32(5), ic32(offCaller), lget(0), call(fnSetState),
|
||||
ic32(offInitedPfx), ic32(14), ic32(offCaller), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// propose(key, value) — store pending proposal
|
||||
// Locals: keyLen(0), valLen(1), keyBufLen(2)
|
||||
func proposeBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(3, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
ic32(1), ic32(offArg1), ic32(256), call(fnGetArgStr), lset(1),
|
||||
lget(1), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// set state["prop:<key>"] = value
|
||||
ic32(offPfxProp), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(2),
|
||||
ic32(offScratch), lget(2), ic32(offArg1), lget(1), call(fnSetState),
|
||||
|
||||
// log "proposed: <key>"
|
||||
ic32(offProposedPfx), ic32(9), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// approve(key) — admin: move prop:<key> → param:<key>
|
||||
// Locals: keyLen(0), callerLen(1), adminLen(2), propLen(3), keyBufLen(4)
|
||||
func approveBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(5, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
isCallerAdminCode(1, 2),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Read pending value: get_state("prop:<key>", offValRead, 256)
|
||||
ic32(offPfxProp), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offValRead), ic32(256), call(fnGetState), lset(3),
|
||||
lget(3), i32Eqz(), if_(),
|
||||
ic32(offNoPendingPfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// set state["param:<key>"] = offValRead[0..propLen)
|
||||
ic32(offPfxParam), ic32(6), ic32(offArg0), lget(0), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offValRead), lget(3), call(fnSetState),
|
||||
|
||||
// delete pending: set state["prop:<key>"] = ""
|
||||
ic32(offPfxProp), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offScratch), ic32(0), call(fnSetState),
|
||||
|
||||
ic32(offApprovedPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// reject(key) — admin: delete prop:<key>
|
||||
// Locals: keyLen(0), callerLen(1), adminLen(2), keyBufLen(3)
|
||||
func rejectBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(4, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
isCallerAdminCode(1, 2),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// delete pending
|
||||
ic32(offPfxProp), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offScratch), ic32(0), call(fnSetState),
|
||||
ic32(offRejectedPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// get(key) — read live parameter value
|
||||
// Locals: keyLen(0), valLen(1), keyBufLen(2)
|
||||
func getBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(3, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
ic32(offPfxParam), ic32(6), ic32(offArg0), lget(0), call(fnBuildKey), lset(2),
|
||||
ic32(offScratch), lget(2), ic32(offValRead), ic32(256), call(fnGetState), lset(1),
|
||||
lget(1), i32Eqz(), if_(),
|
||||
ic32(offNotSetPfx), ic32(9), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offValuePfx), ic32(7), ic32(offValRead), lget(1), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// get_pending(key) — read pending proposal
|
||||
// Locals: keyLen(0), valLen(1), keyBufLen(2)
|
||||
func getPendingBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(3, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
ic32(offPfxProp), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(2),
|
||||
ic32(offScratch), lget(2), ic32(offValRead), ic32(256), call(fnGetState), lset(1),
|
||||
lget(1), i32Eqz(), if_(),
|
||||
ic32(offNoPendingPfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offPendingPfx), ic32(9), ic32(offValRead), lget(1), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// set_admin(new_admin) — transfer admin role
|
||||
// Locals: newAdminLen(0), callerLen(1), adminLen(2)
|
||||
func setAdminBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(3, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(128), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
isCallerAdminCode(1, 2),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
ic32(offKeyAdmin), ic32(5), ic32(offArg0), lget(0), call(fnSetState),
|
||||
ic32(offAdminPfx), ic32(7), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func main() {
|
||||
// Types:
|
||||
// 0: (i32,i32,i32)→(i32) get_arg_str, bytes_equal
|
||||
// 1: (i32,i32)→(i32) get_caller
|
||||
// 2: (i32,i32,i32,i32)→(i32) get_state, build_key
|
||||
// 3: (i32,i32,i32,i32)→() set_state, log_prefix_name
|
||||
// 4: (i32,i32)→() log
|
||||
// 5: ()→() exported methods
|
||||
// 6: (i32,i32,i32)→() memcpy
|
||||
typeSection := section(0x01, vec(
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{tI32}), // 0
|
||||
functype([]byte{tI32, tI32}, []byte{tI32}), // 1
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{tI32}), // 2
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{}), // 3
|
||||
functype([]byte{tI32, tI32}, []byte{}), // 4
|
||||
functype([]byte{}, []byte{}), // 5
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{}), // 6
|
||||
))
|
||||
|
||||
importSection := section(0x02, vec(
|
||||
importFunc("env", "get_arg_str", 0), // 0 type 0
|
||||
importFunc("env", "get_caller", 1), // 1 type 1
|
||||
importFunc("env", "get_state", 2), // 2 type 2
|
||||
importFunc("env", "set_state", 3), // 3 type 3
|
||||
importFunc("env", "log", 4), // 4 type 4
|
||||
))
|
||||
|
||||
// 11 local functions
|
||||
functionSection := section(0x03, vec(
|
||||
u(0), // bytes_equal type 0
|
||||
u(6), // memcpy type 6
|
||||
u(3), // log_prefix_name type 3
|
||||
u(2), // build_key type 2
|
||||
u(5), // init type 5
|
||||
u(5), // propose type 5
|
||||
u(5), // approve type 5
|
||||
u(5), // reject type 5
|
||||
u(5), // get type 5
|
||||
u(5), // get_pending type 5
|
||||
u(5), // set_admin type 5
|
||||
))
|
||||
|
||||
memorySection := section(0x05, vec(cat([]byte{0x00}, u(2)))) // 2 pages = 128KB
|
||||
|
||||
exportSection := section(0x07, vec(
|
||||
exportEntry("memory", 0x02, 0),
|
||||
exportEntry("init", 0x00, fnInit),
|
||||
exportEntry("propose", 0x00, fnPropose),
|
||||
exportEntry("approve", 0x00, fnApprove),
|
||||
exportEntry("reject", 0x00, fnReject),
|
||||
exportEntry("get", 0x00, fnGet),
|
||||
exportEntry("get_pending", 0x00, fnGetPending),
|
||||
exportEntry("set_admin", 0x00, fnSetAdmin),
|
||||
))
|
||||
|
||||
dataSection := section(0x0B, cat(
|
||||
u(14),
|
||||
dataSegment(offKeyAdmin, []byte("admin")),
|
||||
dataSegment(offPfxParam, []byte("param:")),
|
||||
dataSegment(offPfxProp, []byte("prop:")),
|
||||
dataSegment(offInitedPfx, []byte("initialized: ")),
|
||||
dataSegment(offProposedPfx, []byte("proposed: ")),
|
||||
dataSegment(offApprovedPfx, []byte("approved: ")),
|
||||
dataSegment(offRejectedPfx, []byte("rejected: ")),
|
||||
dataSegment(offValuePfx, []byte("value: ")),
|
||||
dataSegment(offNotSetPfx, []byte("not set: ")),
|
||||
dataSegment(offPendingPfx, []byte("pending: ")),
|
||||
dataSegment(offNoPendingPfx, []byte("no pending: ")),
|
||||
dataSegment(offAdminPfx, []byte("admin: ")),
|
||||
dataSegment(offUnauthPfx, []byte("unauthorized: ")),
|
||||
dataSegment(offAlreadyPfx, []byte("already init")),
|
||||
))
|
||||
|
||||
codeSection := section(0x0A, cat(
|
||||
u(11),
|
||||
bytesEqualBody(),
|
||||
memcpyBody(),
|
||||
logPrefixNameBody(),
|
||||
buildKeyBody(),
|
||||
initBody(),
|
||||
proposeBody(),
|
||||
approveBody(),
|
||||
rejectBody(),
|
||||
getBody(),
|
||||
getPendingBody(),
|
||||
setAdminBody(),
|
||||
))
|
||||
|
||||
module := cat(
|
||||
[]byte{0x00, 0x61, 0x73, 0x6d},
|
||||
[]byte{0x01, 0x00, 0x00, 0x00},
|
||||
typeSection,
|
||||
importSection,
|
||||
functionSection,
|
||||
memorySection,
|
||||
exportSection,
|
||||
dataSection,
|
||||
codeSection,
|
||||
)
|
||||
|
||||
out := "contracts/governance/governance.wasm"
|
||||
if err := os.WriteFile(out, module, 0644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "write:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Written %s (%d bytes)\n", out, len(module))
|
||||
}
|
||||
BIN
contracts/governance/governance.wasm
Normal file
BIN
contracts/governance/governance.wasm
Normal file
Binary file not shown.
55
contracts/governance/governance_abi.json
Normal file
55
contracts/governance/governance_abi.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"contract": "governance",
|
||||
"version": "1.0.0",
|
||||
"description": "On-chain parameter governance. The deployer becomes the admin. Anyone can propose a parameter change; the admin approves or rejects it. Used to manage gas_price, messenger_entry_fee, relay_fee, etc.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "init",
|
||||
"description": "Initialize the contract, setting the caller as the admin. Must be called once after deployment.",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "propose",
|
||||
"description": "Submit a proposal to change a named parameter. Anyone can propose. Logs 'proposed: <key>=<value>'.",
|
||||
"args": [
|
||||
{"name": "key", "type": "string", "description": "Parameter name (max 64 chars)"},
|
||||
{"name": "value", "type": "string", "description": "Proposed new value (max 256 chars)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "approve",
|
||||
"description": "Admin approves the pending proposal for a key, committing it as the live value. Logs 'approved: <key>'.",
|
||||
"args": [
|
||||
{"name": "key", "type": "string", "description": "Parameter key to approve"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "reject",
|
||||
"description": "Admin rejects and removes the pending proposal for a key. Logs 'rejected: <key>'.",
|
||||
"args": [
|
||||
{"name": "key", "type": "string", "description": "Parameter key to reject"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "get",
|
||||
"description": "Read the current live value of a parameter. Logs 'value: <value>' or 'not set: <key>'.",
|
||||
"args": [
|
||||
{"name": "key", "type": "string", "description": "Parameter key"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "get_pending",
|
||||
"description": "Read the pending proposed value. Logs 'pending: <value>' or 'no pending: <key>'.",
|
||||
"args": [
|
||||
{"name": "key", "type": "string", "description": "Parameter key"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "set_admin",
|
||||
"description": "Transfer the admin role to a new address. Only the current admin may call this. Logs 'admin: <new_admin>'.",
|
||||
"args": [
|
||||
{"name": "new_admin", "type": "string", "description": "New admin address (hex pubkey)"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
38
contracts/hello_go/hello_go_abi.json
Normal file
38
contracts/hello_go/hello_go_abi.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"contract": "hello_go",
|
||||
"version": "1.0.0",
|
||||
"description": "Example DChain contract written in Go (TinyGo SDK). Demonstrates counter, owner-gated reset, string args, and inter-contract calls.",
|
||||
"build": "tinygo build -o hello_go.wasm -target wasip1 -no-debug .",
|
||||
"methods": [
|
||||
{
|
||||
"name": "increment",
|
||||
"description": "Add 1 to the counter. Logs 'counter: N'.",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "get",
|
||||
"description": "Log the current counter value without changing state.",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "reset",
|
||||
"description": "Reset counter to 0. First caller becomes the owner; only owner can reset.",
|
||||
"args": []
|
||||
},
|
||||
{
|
||||
"name": "greet",
|
||||
"description": "Log a greeting. Logs 'hello, <name>! block=N'.",
|
||||
"args": [
|
||||
{"name": "name", "type": "string", "description": "Name to greet (defaults to 'world')"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ping",
|
||||
"description": "Call a method on another contract (inter-contract call demo).",
|
||||
"args": [
|
||||
{"name": "contract_id", "type": "string", "description": "Target contract ID"},
|
||||
{"name": "method", "type": "string", "description": "Method to call on target"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
104
contracts/hello_go/main.go
Normal file
104
contracts/hello_go/main.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Package main is an example DChain smart contract written in Go.
|
||||
//
|
||||
// # Build
|
||||
//
|
||||
// tinygo build -o hello_go.wasm -target wasip1 -no-debug .
|
||||
//
|
||||
// # Deploy
|
||||
//
|
||||
// client deploy-contract --key /keys/node1.json \
|
||||
// --wasm hello_go.wasm --abi hello_go_abi.json \
|
||||
// --node http://localhost:8081
|
||||
//
|
||||
// # Use
|
||||
//
|
||||
// client call-contract --key /keys/node1.json --contract $ID \
|
||||
// --method increment --gas 5000 --node http://localhost:8081
|
||||
//
|
||||
// The contract implements a simple counter with owner-gated reset.
|
||||
// It demonstrates every SDK primitive: arguments, state, caller, logging,
|
||||
// and inter-contract calls.
|
||||
package main
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
dc "go-blockchain/contracts/sdk"
|
||||
)
|
||||
|
||||
// increment adds 1 to the counter and logs the new value.
|
||||
//
|
||||
//export increment
|
||||
func increment() {
|
||||
v := dc.GetU64("counter")
|
||||
v++
|
||||
dc.PutU64("counter", v)
|
||||
dc.Log("counter: " + strconv.FormatUint(v, 10))
|
||||
}
|
||||
|
||||
// get logs the current counter value without changing state.
|
||||
//
|
||||
//export get
|
||||
func get() {
|
||||
v := dc.GetU64("counter")
|
||||
dc.Log("counter: " + strconv.FormatUint(v, 10))
|
||||
}
|
||||
|
||||
// reset sets the counter to 0. On first call the caller becomes the owner.
|
||||
// Subsequent calls are restricted to the owner.
|
||||
//
|
||||
//export reset
|
||||
func reset() {
|
||||
owner := dc.GetStateStr("owner")
|
||||
caller := dc.Caller()
|
||||
if owner == "" {
|
||||
dc.SetStateStr("owner", caller)
|
||||
dc.PutU64("counter", 0)
|
||||
dc.Log("initialized — owner: " + caller[:min8(caller)])
|
||||
return
|
||||
}
|
||||
if caller != owner {
|
||||
dc.Log("unauthorized: " + caller[:min8(caller)])
|
||||
return
|
||||
}
|
||||
dc.PutU64("counter", 0)
|
||||
dc.Log("reset by owner")
|
||||
}
|
||||
|
||||
// greet logs a personalised greeting using the first call argument.
|
||||
//
|
||||
//export greet
|
||||
func greet() {
|
||||
name := dc.ArgStr(0, 64)
|
||||
if name == "" {
|
||||
name = "world"
|
||||
}
|
||||
dc.Log("hello, " + name + "! block=" + strconv.FormatUint(dc.BlockHeight(), 10))
|
||||
}
|
||||
|
||||
// ping calls another contract's method (demonstrates inter-contract calls).
|
||||
// Args: contract_id (string), method (string)
|
||||
//
|
||||
//export ping
|
||||
func ping() {
|
||||
target := dc.ArgStr(0, 64)
|
||||
method := dc.ArgStr(1, 64)
|
||||
if target == "" || method == "" {
|
||||
dc.Log("ping: target and method required")
|
||||
return
|
||||
}
|
||||
if dc.CallContract(target, method, "[]") {
|
||||
dc.Log("ping: " + method + " ok")
|
||||
} else {
|
||||
dc.Log("ping: " + method + " failed")
|
||||
}
|
||||
}
|
||||
|
||||
func min8(s string) int {
|
||||
if len(s) < 8 {
|
||||
return len(s)
|
||||
}
|
||||
return 8
|
||||
}
|
||||
|
||||
func main() {}
|
||||
458
contracts/name_registry/gen/main.go
Normal file
458
contracts/name_registry/gen/main.go
Normal file
@@ -0,0 +1,458 @@
|
||||
// gen generates contracts/name_registry/name_registry.wasm
|
||||
// Run from the repo root: go run ./contracts/name_registry/gen/
|
||||
//
|
||||
// Contract methods: register, resolve, transfer, release
|
||||
// Host imports from "env": get_arg_str, get_caller, get_state, set_state, log
|
||||
//
|
||||
// Every contract action emits a human-readable log entry visible in the
|
||||
// block explorer, e.g.:
|
||||
// "registered: alice"
|
||||
// "name taken: alice"
|
||||
// "not found: alice"
|
||||
// "owner: <pubkey>"
|
||||
// "transferred: alice"
|
||||
// "unauthorized: alice"
|
||||
// "released: alice"
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ── LEB128 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func u(v uint64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
if v != 0 {
|
||||
bt |= 0x80
|
||||
}
|
||||
b = append(b, bt)
|
||||
if v == 0 {
|
||||
return b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func s(v int64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
sign := (bt & 0x40) != 0
|
||||
if (v == 0 && !sign) || (v == -1 && sign) {
|
||||
return append(b, bt)
|
||||
}
|
||||
b = append(b, bt|0x80)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Builders ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func cat(slices ...[]byte) []byte {
|
||||
var out []byte
|
||||
for _, sl := range slices {
|
||||
out = append(out, sl...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func wstr(str string) []byte { return cat(u(uint64(len(str))), []byte(str)) }
|
||||
|
||||
func section(id byte, content []byte) []byte {
|
||||
return cat([]byte{id}, u(uint64(len(content))), content)
|
||||
}
|
||||
|
||||
func vec(items ...[]byte) []byte {
|
||||
out := u(uint64(len(items)))
|
||||
for _, it := range items {
|
||||
out = append(out, it...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func functype(params, results []byte) []byte {
|
||||
return cat([]byte{0x60}, u(uint64(len(params))), params, u(uint64(len(results))), results)
|
||||
}
|
||||
|
||||
func importFunc(mod, name string, typeIdx uint32) []byte {
|
||||
return cat(wstr(mod), wstr(name), []byte{0x00}, u(uint64(typeIdx)))
|
||||
}
|
||||
|
||||
func exportEntry(name string, kind byte, idx uint32) []byte {
|
||||
return cat(wstr(name), []byte{kind}, u(uint64(idx)))
|
||||
}
|
||||
|
||||
func dataSegment(offset int32, data []byte) []byte {
|
||||
return cat(
|
||||
[]byte{0x00},
|
||||
[]byte{0x41}, s(int64(offset)), []byte{0x0B},
|
||||
u(uint64(len(data))), data,
|
||||
)
|
||||
}
|
||||
|
||||
func funcBody(localDecls []byte, instrs ...[]byte) []byte {
|
||||
inner := cat(localDecls)
|
||||
for _, ins := range instrs {
|
||||
inner = append(inner, ins...)
|
||||
}
|
||||
inner = append(inner, 0x0B) // end
|
||||
return cat(u(uint64(len(inner))), inner)
|
||||
}
|
||||
|
||||
var noLocals = u(0)
|
||||
|
||||
func localDecl(n uint32, typ byte) []byte { return cat(u(uint64(n)), []byte{typ}) }
|
||||
func withLocals(decls ...[]byte) []byte {
|
||||
return cat(u(uint64(len(decls))), cat(decls...))
|
||||
}
|
||||
|
||||
// ── Instructions ──────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
tI32 byte = 0x7F
|
||||
tI64 byte = 0x7E
|
||||
)
|
||||
|
||||
func call(fn uint32) []byte { return cat([]byte{0x10}, u(uint64(fn))) }
|
||||
func lget(i uint32) []byte { return cat([]byte{0x20}, u(uint64(i))) }
|
||||
func lset(i uint32) []byte { return cat([]byte{0x21}, u(uint64(i))) }
|
||||
func ic32(v int32) []byte { return cat([]byte{0x41}, s(int64(v))) }
|
||||
func block_() []byte { return []byte{0x02, 0x40} }
|
||||
func loop_() []byte { return []byte{0x03, 0x40} }
|
||||
func if_() []byte { return []byte{0x04, 0x40} }
|
||||
func ifI32() []byte { return []byte{0x04, tI32} } // if that returns i32
|
||||
func else_() []byte { return []byte{0x05} }
|
||||
func end_() []byte { return []byte{0x0B} }
|
||||
func br_(lbl uint32) []byte { return cat([]byte{0x0C}, u(uint64(lbl))) }
|
||||
func brIf_(lbl uint32) []byte { return cat([]byte{0x0D}, u(uint64(lbl))) }
|
||||
func return_() []byte { return []byte{0x0F} }
|
||||
func i32Eqz() []byte { return []byte{0x45} }
|
||||
func i32Ne() []byte { return []byte{0x47} }
|
||||
func i32GtU() []byte { return []byte{0x4B} }
|
||||
func i32GeU() []byte { return []byte{0x4F} }
|
||||
func i32Add() []byte { return []byte{0x6A} }
|
||||
func i32Load8U() []byte { return []byte{0x2D, 0x00, 0x00} }
|
||||
func i32Store8() []byte { return []byte{0x3A, 0x00, 0x00} }
|
||||
|
||||
// ── Memory layout ─────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
offArg0 = 0x000 // arg[0] name buffer (64 bytes)
|
||||
offArg1 = 0x040 // arg[1] new_owner buffer (128 bytes)
|
||||
offCaller = 0x0C0 // caller pubkey buffer (128 bytes)
|
||||
offStateRead = 0x140 // existing owner buffer (128 bytes)
|
||||
|
||||
// Verbose prefix strings — each ends with ": " for readable log messages.
|
||||
offRegisteredPfx = 0x200 // "registered: " 12 bytes
|
||||
offNameTakenPfx = 0x20C // "name taken: " 12 bytes
|
||||
offNotFoundPfx = 0x218 // "not found: " 11 bytes
|
||||
offOwnerPfx = 0x224 // "owner: " 7 bytes
|
||||
offTransferredPfx = 0x22C // "transferred: " 13 bytes
|
||||
offUnauthPfx = 0x23A // "unauthorized: " 14 bytes
|
||||
offReleasedPfx = 0x249 // "released: " 10 bytes
|
||||
|
||||
// Scratch buffer for building concatenated log messages.
|
||||
offScratch = 0x300 // 256 bytes
|
||||
)
|
||||
|
||||
// Import function indices (order must match importSection below)
|
||||
const (
|
||||
fnGetArgStr = 0 // get_arg_str(idx, dstPtr, dstLen i32) → i32
|
||||
fnGetCaller = 1 // get_caller(bufPtr, bufLen i32) → i32
|
||||
fnGetState = 2 // get_state(kP,kL,dP,dL i32) → i32
|
||||
fnSetState = 3 // set_state(kP,kL,vP,vL i32)
|
||||
fnLog = 4 // log(msgPtr, msgLen i32)
|
||||
)
|
||||
|
||||
// Local function indices (imports first, then locals in declaration order)
|
||||
const (
|
||||
fnBytesEqual = 5 // $bytes_equal(aPtr,bPtr,len i32) → i32
|
||||
fnMemcpy = 6 // $memcpy(dst,src,len i32)
|
||||
fnLogPrefixName = 7 // $log_prefix_name(pfxPtr,pfxLen,sfxPtr,sfxLen i32)
|
||||
fnRegister = 8
|
||||
fnResolve = 9
|
||||
fnTransfer = 10
|
||||
fnRelease = 11
|
||||
)
|
||||
|
||||
// ── $bytes_equal helper ───────────────────────────────────────────────────────
|
||||
// (aPtr i32, bPtr i32, len i32) → i32
|
||||
// locals: i(3), same(4)
|
||||
func bytesEqualBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(2, tI32)), // locals 3=i, 4=same
|
||||
// same = 1; i = 0
|
||||
ic32(1), lset(4),
|
||||
ic32(0), lset(3),
|
||||
block_(),
|
||||
loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1), // i >= len → exit block
|
||||
// load mem[aPtr+i]
|
||||
lget(0), lget(3), i32Add(), i32Load8U(),
|
||||
// load mem[bPtr+i]
|
||||
lget(1), lget(3), i32Add(), i32Load8U(),
|
||||
i32Ne(), if_(),
|
||||
ic32(0), lset(4),
|
||||
br_(2), // exit block
|
||||
end_(),
|
||||
lget(3), ic32(1), i32Add(), lset(3),
|
||||
br_(0), // next iteration
|
||||
end_(),
|
||||
end_(),
|
||||
lget(4), // return same
|
||||
)
|
||||
}
|
||||
|
||||
// ── $memcpy helper ────────────────────────────────────────────────────────────
|
||||
// (dst i32, src i32, len i32) — copies len bytes from src to dst.
|
||||
// locals: i(3)
|
||||
func memcpyBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(1, tI32)), // local 3 = i
|
||||
ic32(0), lset(3), // i = 0
|
||||
block_(),
|
||||
loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1), // if i >= len: exit
|
||||
// mem[dst+i] = mem[src+i]
|
||||
lget(0), lget(3), i32Add(), // dst+i (store address)
|
||||
lget(1), lget(3), i32Add(), i32Load8U(), // load mem[src+i]
|
||||
i32Store8(),
|
||||
lget(3), ic32(1), i32Add(), lset(3), // i++
|
||||
br_(0),
|
||||
end_(),
|
||||
end_(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── $log_prefix_name helper ───────────────────────────────────────────────────
|
||||
// (prefixPtr i32, prefixLen i32, suffixPtr i32, suffixLen i32)
|
||||
// Builds "prefix<suffix>" in scratch buffer and logs it.
|
||||
func logPrefixNameBody() []byte {
|
||||
return funcBody(
|
||||
noLocals,
|
||||
// memcpy(offScratch, prefixPtr, prefixLen)
|
||||
ic32(offScratch), lget(0), lget(1), call(fnMemcpy),
|
||||
// memcpy(offScratch + prefixLen, suffixPtr, suffixLen)
|
||||
ic32(offScratch), lget(1), i32Add(), lget(2), lget(3), call(fnMemcpy),
|
||||
// log(offScratch, prefixLen + suffixLen)
|
||||
ic32(offScratch), lget(1), lget(3), i32Add(), call(fnLog),
|
||||
)
|
||||
}
|
||||
|
||||
// ── isOwner: shared caller-vs-existing check ──────────────────────────────────
|
||||
// Assumes: caller is at offCaller with len callerLenLocal,
|
||||
// existing is at offStateRead with len existingLenLocal.
|
||||
// Returns instructions that leave i32 (1=same, 0=not) on stack.
|
||||
func isOwnerCheck(callerLenLocal, existingLenLocal uint32) []byte {
|
||||
return cat(
|
||||
// if callerLen != existingLen → push 0
|
||||
ifI32(),
|
||||
ic32(0),
|
||||
else_(),
|
||||
// else call bytes_equal(offCaller, offStateRead, callerLen)
|
||||
ic32(offCaller), ic32(offStateRead), lget(callerLenLocal),
|
||||
call(fnBytesEqual),
|
||||
end_(),
|
||||
)
|
||||
}
|
||||
|
||||
func main() {
|
||||
// ── Type section ──────────────────────────────────────────────────────────
|
||||
// Type 0: (i32,i32,i32)→(i32) get_arg_str, bytes_equal
|
||||
// Type 1: (i32,i32)→(i32) get_caller
|
||||
// Type 2: (i32,i32,i32,i32)→(i32) get_state
|
||||
// Type 3: (i32,i32,i32,i32)→() set_state, log_prefix_name
|
||||
// Type 4: (i32,i32)→() log
|
||||
// Type 5: ()→() register, resolve, transfer, release
|
||||
// Type 6: (i32,i32,i32)→() memcpy
|
||||
typeSection := section(0x01, vec(
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{tI32}), // 0
|
||||
functype([]byte{tI32, tI32}, []byte{tI32}), // 1
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{tI32}), // 2
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{}), // 3
|
||||
functype([]byte{tI32, tI32}, []byte{}), // 4
|
||||
functype([]byte{}, []byte{}), // 5
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{}), // 6
|
||||
))
|
||||
|
||||
// ── Import section ────────────────────────────────────────────────────────
|
||||
importSection := section(0x02, vec(
|
||||
importFunc("env", "get_arg_str", 0), // fnGetArgStr=0 type 0
|
||||
importFunc("env", "get_caller", 1), // fnGetCaller=1 type 1
|
||||
importFunc("env", "get_state", 2), // fnGetState=2 type 2
|
||||
importFunc("env", "set_state", 3), // fnSetState=3 type 3
|
||||
importFunc("env", "log", 4), // fnLog=4 type 4
|
||||
))
|
||||
|
||||
// ── Function section: 7 local functions ──────────────────────────────────
|
||||
functionSection := section(0x03, vec(
|
||||
u(0), // bytes_equal → type 0
|
||||
u(6), // memcpy → type 6
|
||||
u(3), // log_prefix_name → type 3
|
||||
u(5), // register → type 5
|
||||
u(5), // resolve → type 5
|
||||
u(5), // transfer → type 5
|
||||
u(5), // release → type 5
|
||||
))
|
||||
|
||||
// ── Memory section ────────────────────────────────────────────────────────
|
||||
memorySection := section(0x05, vec(cat([]byte{0x00}, u(1))))
|
||||
|
||||
// ── Export section ────────────────────────────────────────────────────────
|
||||
exportSection := section(0x07, vec(
|
||||
exportEntry("memory", 0x02, 0),
|
||||
exportEntry("register", 0x00, fnRegister),
|
||||
exportEntry("resolve", 0x00, fnResolve),
|
||||
exportEntry("transfer", 0x00, fnTransfer),
|
||||
exportEntry("release", 0x00, fnRelease),
|
||||
))
|
||||
|
||||
// ── Data section ──────────────────────────────────────────────────────────
|
||||
dataSection := section(0x0B, cat(
|
||||
u(7),
|
||||
dataSegment(offRegisteredPfx, []byte("registered: ")),
|
||||
dataSegment(offNameTakenPfx, []byte("name taken: ")),
|
||||
dataSegment(offNotFoundPfx, []byte("not found: ")),
|
||||
dataSegment(offOwnerPfx, []byte("owner: ")),
|
||||
dataSegment(offTransferredPfx, []byte("transferred: ")),
|
||||
dataSegment(offUnauthPfx, []byte("unauthorized: ")),
|
||||
dataSegment(offReleasedPfx, []byte("released: ")),
|
||||
))
|
||||
|
||||
// ── Code section ─────────────────────────────────────────────────────────
|
||||
|
||||
// register(): locals nameLen(0), callerLen(1), existingLen(2)
|
||||
registerBody := funcBody(
|
||||
withLocals(localDecl(3, tI32)),
|
||||
// nameLen = get_arg_str(0, offArg0, 64)
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
// if nameLen == 0: return
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
// existingLen = get_state(offArg0, nameLen, offStateRead, 128)
|
||||
ic32(offArg0), lget(0), ic32(offStateRead), ic32(128), call(fnGetState), lset(2),
|
||||
// if existingLen > 0: log("name taken: <name>"); return
|
||||
lget(2), ic32(0), i32GtU(), if_(),
|
||||
ic32(offNameTakenPfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
// callerLen = get_caller(offCaller, 128)
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
// set_state(offArg0, nameLen, offCaller, callerLen)
|
||||
ic32(offArg0), lget(0), ic32(offCaller), lget(1), call(fnSetState),
|
||||
// log("registered: <name>")
|
||||
ic32(offRegisteredPfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
|
||||
// resolve(): locals nameLen(0), ownerLen(1)
|
||||
resolveBody := funcBody(
|
||||
withLocals(localDecl(2, tI32)),
|
||||
// nameLen = get_arg_str(0, offArg0, 64)
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
// ownerLen = get_state(offArg0, nameLen, offStateRead, 128)
|
||||
ic32(offArg0), lget(0), ic32(offStateRead), ic32(128), call(fnGetState), lset(1),
|
||||
// if ownerLen == 0: log("not found: <name>"); return
|
||||
lget(1), i32Eqz(), if_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
// log("owner: <pubkey>")
|
||||
// The pubkey stored in state is the raw caller string (hex-encoded),
|
||||
// so the log will display the human-readable address.
|
||||
ic32(offOwnerPfx), ic32(7), ic32(offStateRead), lget(1), call(fnLogPrefixName),
|
||||
)
|
||||
|
||||
// transfer(): locals nameLen(0), newOwnerLen(1), callerLen(2), existingLen(3)
|
||||
transferBody := funcBody(
|
||||
withLocals(localDecl(4, tI32)),
|
||||
// nameLen = get_arg_str(0, offArg0, 64)
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
// newOwnerLen = get_arg_str(1, offArg1, 128)
|
||||
ic32(1), ic32(offArg1), ic32(128), call(fnGetArgStr), lset(1),
|
||||
lget(1), i32Eqz(), if_(), return_(), end_(),
|
||||
// existingLen = get_state(offArg0, nameLen, offStateRead, 128)
|
||||
ic32(offArg0), lget(0), ic32(offStateRead), ic32(128), call(fnGetState), lset(3),
|
||||
// if existingLen == 0: not registered → anyone can claim
|
||||
lget(3), i32Eqz(), if_(),
|
||||
ic32(offArg0), lget(0), ic32(offArg1), lget(1), call(fnSetState),
|
||||
ic32(offTransferredPfx), ic32(13), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
// callerLen = get_caller(offCaller, 128)
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(2),
|
||||
// isOwner = (callerLen == existingLen) ? bytes_equal(...) : 0
|
||||
lget(2), lget(3), i32Ne(),
|
||||
isOwnerCheck(2, 3),
|
||||
// if !isOwner: log("unauthorized: <name>"); return
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
// Authorized
|
||||
ic32(offArg0), lget(0), ic32(offArg1), lget(1), call(fnSetState),
|
||||
ic32(offTransferredPfx), ic32(13), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
|
||||
// release(): locals nameLen(0), callerLen(1), existingLen(2)
|
||||
releaseBody := funcBody(
|
||||
withLocals(localDecl(3, tI32)),
|
||||
// nameLen = get_arg_str(0, offArg0, 64)
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
// existingLen = get_state(offArg0, nameLen, offStateRead, 128)
|
||||
ic32(offArg0), lget(0), ic32(offStateRead), ic32(128), call(fnGetState), lset(2),
|
||||
// if existingLen == 0: log("not found: <name>"); return
|
||||
lget(2), i32Eqz(), if_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
// callerLen = get_caller(offCaller, 128)
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
// isOwner check
|
||||
lget(1), lget(2), i32Ne(),
|
||||
isOwnerCheck(1, 2),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
// Store empty value → effectively releases the name
|
||||
ic32(offArg0), lget(0), ic32(offArg0), ic32(0), call(fnSetState),
|
||||
ic32(offReleasedPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
|
||||
codeSection := section(0x0A, cat(
|
||||
u(7),
|
||||
bytesEqualBody(),
|
||||
memcpyBody(),
|
||||
logPrefixNameBody(),
|
||||
registerBody,
|
||||
resolveBody,
|
||||
transferBody,
|
||||
releaseBody,
|
||||
))
|
||||
|
||||
// ── Assemble module ───────────────────────────────────────────────────────
|
||||
module := cat(
|
||||
[]byte{0x00, 0x61, 0x73, 0x6d}, // \0asm
|
||||
[]byte{0x01, 0x00, 0x00, 0x00}, // version 1
|
||||
typeSection,
|
||||
importSection,
|
||||
functionSection,
|
||||
memorySection,
|
||||
exportSection,
|
||||
dataSection,
|
||||
codeSection,
|
||||
)
|
||||
|
||||
out := "contracts/name_registry/name_registry.wasm"
|
||||
if err := os.WriteFile(out, module, 0644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "write:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Written %s (%d bytes)\n", out, len(module))
|
||||
}
|
||||
BIN
contracts/name_registry/name_registry.wasm
Normal file
BIN
contracts/name_registry/name_registry.wasm
Normal file
Binary file not shown.
301
contracts/name_registry/name_registry.wat
Normal file
301
contracts/name_registry/name_registry.wat
Normal file
@@ -0,0 +1,301 @@
|
||||
(module
|
||||
;; Name Registry smart contract
|
||||
;;
|
||||
;; Maps human-readable names → owner public keys on-chain.
|
||||
;; Each name can only be registered once; only the current owner can
|
||||
;; transfer or release it.
|
||||
;;
|
||||
;; Methods (all void, no WASM return values):
|
||||
;; register(name string) — claim a name for the caller
|
||||
;; resolve(name string) — log the current owner pubkey
|
||||
;; transfer(name string, new_owner string) — give name to another pubkey
|
||||
;; release(name string) — delete name registration
|
||||
;;
|
||||
;; State keys: the raw name bytes → owner pubkey bytes.
|
||||
;; All args come in via env.get_arg_str / env.get_arg_u64.
|
||||
|
||||
;; ── imports ──────────────────────────────────────────────────────────────
|
||||
(import "env" "get_arg_str"
|
||||
(func $get_arg_str (param i32 i32 i32) (result i32)))
|
||||
(import "env" "get_caller"
|
||||
(func $get_caller (param i32 i32) (result i32)))
|
||||
(import "env" "get_state"
|
||||
(func $get_state (param i32 i32 i32 i32) (result i32)))
|
||||
(import "env" "set_state"
|
||||
(func $set_state (param i32 i32 i32 i32)))
|
||||
(import "env" "log"
|
||||
(func $log (param i32 i32)))
|
||||
|
||||
;; ── memory ───────────────────────────────────────────────────────────────
|
||||
;; Offset Size Purpose
|
||||
;; 0x000 64 arg[0] buffer — name (max 64 bytes)
|
||||
;; 0x040 128 arg[1] buffer — new_owner pubkey (max 128 bytes)
|
||||
;; 0x0C0 128 caller pubkey buffer
|
||||
;; 0x140 128 state-read buffer (existing owner)
|
||||
;; 0x200 ~128 verbose log prefix strings
|
||||
;; 0x300 256 scratch buffer — build "prefix: name" log messages
|
||||
(memory (export "memory") 1)
|
||||
|
||||
;; ── verbose log prefix strings ───────────────────────────────────────────
|
||||
;; Each entry is a human-readable prefix ending with ": " so that the
|
||||
;; log message becomes "prefix: <argument>" — readable in the explorer.
|
||||
;;
|
||||
;; "registered: " 12 bytes @ 0x200
|
||||
;; "name taken: " 12 bytes @ 0x20C
|
||||
;; "not found: " 11 bytes @ 0x218
|
||||
;; "owner: " 7 bytes @ 0x224
|
||||
;; "transferred: " 13 bytes @ 0x22C
|
||||
;; "unauthorized: " 14 bytes @ 0x23A
|
||||
;; "released: " 10 bytes @ 0x249
|
||||
(data (i32.const 0x200) "registered: ")
|
||||
(data (i32.const 0x20C) "name taken: ")
|
||||
(data (i32.const 0x218) "not found: ")
|
||||
(data (i32.const 0x224) "owner: ")
|
||||
(data (i32.const 0x22C) "transferred: ")
|
||||
(data (i32.const 0x23A) "unauthorized: ")
|
||||
(data (i32.const 0x249) "released: ")
|
||||
|
||||
;; ── helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
;; $memcpy: copy len bytes from src to dst
|
||||
(func $memcpy (param $dst i32) (param $src i32) (param $len i32)
|
||||
(local $i i32)
|
||||
(local.set $i (i32.const 0))
|
||||
(block $break
|
||||
(loop $loop
|
||||
(br_if $break (i32.ge_u (local.get $i) (local.get $len)))
|
||||
(i32.store8
|
||||
(i32.add (local.get $dst) (local.get $i))
|
||||
(i32.load8_u (i32.add (local.get $src) (local.get $i))))
|
||||
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
||||
(br $loop)
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
;; $log_prefix_name: build "<prefix><suffix>" in scratch buf 0x300 and log it.
|
||||
;; prefixPtr / prefixLen — the prefix string (e.g. "registered: ", 12 bytes)
|
||||
;; suffixPtr / suffixLen — the name / pubkey to append
|
||||
(func $log_prefix_name
|
||||
(param $prefixPtr i32) (param $prefixLen i32)
|
||||
(param $suffixPtr i32) (param $suffixLen i32)
|
||||
;; copy prefix → scratch[0]
|
||||
(call $memcpy (i32.const 0x300) (local.get $prefixPtr) (local.get $prefixLen))
|
||||
;; copy suffix → scratch[prefixLen]
|
||||
(call $memcpy
|
||||
(i32.add (i32.const 0x300) (local.get $prefixLen))
|
||||
(local.get $suffixPtr)
|
||||
(local.get $suffixLen))
|
||||
;; log scratch[0 .. prefixLen+suffixLen)
|
||||
(call $log
|
||||
(i32.const 0x300)
|
||||
(i32.add (local.get $prefixLen) (local.get $suffixLen)))
|
||||
)
|
||||
|
||||
;; $bytes_equal: compare mem[aPtr..aPtr+len) with mem[bPtr..bPtr+len)
|
||||
;; Params: aPtr(0) bPtr(1) len(2)
|
||||
;; Result: i32 1 = equal, 0 = not equal
|
||||
(func $bytes_equal (param i32 i32 i32) (result i32)
|
||||
(local $i i32)
|
||||
(local $same i32)
|
||||
(local.set $same (i32.const 1))
|
||||
(local.set $i (i32.const 0))
|
||||
(block $break
|
||||
(loop $loop
|
||||
(br_if $break (i32.ge_u (local.get $i) (local.get 2)))
|
||||
(if (i32.ne
|
||||
(i32.load8_u (i32.add (local.get 0) (local.get $i)))
|
||||
(i32.load8_u (i32.add (local.get 1) (local.get $i))))
|
||||
(then (local.set $same (i32.const 0)) (br $break))
|
||||
)
|
||||
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
||||
(br $loop)
|
||||
)
|
||||
)
|
||||
(local.get $same)
|
||||
)
|
||||
|
||||
;; ── register(name) ────────────────────────────────────────────────────────
|
||||
;; Claims `name` for the caller.
|
||||
;; Logs "registered: <name>" on success, "name taken: <name>" on conflict.
|
||||
(func (export "register")
|
||||
(local $nameLen i32)
|
||||
(local $callerLen i32)
|
||||
(local $existingLen i32)
|
||||
|
||||
;; Read name into 0x000, max 64 bytes
|
||||
(local.set $nameLen
|
||||
(call $get_arg_str (i32.const 0) (i32.const 0x000) (i32.const 64)))
|
||||
(if (i32.eqz (local.get $nameLen)) (then return))
|
||||
|
||||
;; Check if name is already taken
|
||||
(local.set $existingLen
|
||||
(call $get_state
|
||||
(i32.const 0x000) (local.get $nameLen)
|
||||
(i32.const 0x140) (i32.const 128)))
|
||||
(if (i32.gt_u (local.get $existingLen) (i32.const 0))
|
||||
(then
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x20C) (i32.const 12) ;; "name taken: "
|
||||
(i32.const 0x000) (local.get $nameLen))
|
||||
return
|
||||
)
|
||||
)
|
||||
|
||||
;; Store: state[name] = caller_pubkey
|
||||
(local.set $callerLen
|
||||
(call $get_caller (i32.const 0x0C0) (i32.const 128)))
|
||||
(call $set_state
|
||||
(i32.const 0x000) (local.get $nameLen)
|
||||
(i32.const 0x0C0) (local.get $callerLen))
|
||||
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x200) (i32.const 12) ;; "registered: "
|
||||
(i32.const 0x000) (local.get $nameLen))
|
||||
)
|
||||
|
||||
;; ── resolve(name) ─────────────────────────────────────────────────────────
|
||||
;; Logs "owner: <pubkey>" for the registered name, or "not found: <name>".
|
||||
(func (export "resolve")
|
||||
(local $nameLen i32)
|
||||
(local $ownerLen i32)
|
||||
|
||||
(local.set $nameLen
|
||||
(call $get_arg_str (i32.const 0) (i32.const 0x000) (i32.const 64)))
|
||||
(if (i32.eqz (local.get $nameLen)) (then return))
|
||||
|
||||
(local.set $ownerLen
|
||||
(call $get_state
|
||||
(i32.const 0x000) (local.get $nameLen)
|
||||
(i32.const 0x140) (i32.const 128)))
|
||||
(if (i32.eqz (local.get $ownerLen))
|
||||
(then
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x218) (i32.const 11) ;; "not found: "
|
||||
(i32.const 0x000) (local.get $nameLen))
|
||||
return
|
||||
)
|
||||
)
|
||||
;; Log "owner: <pubkey bytes>"
|
||||
;; The pubkey stored in state is the raw caller pubkey bytes that the
|
||||
;; host wrote via get_caller — these are the hex-encoded public key
|
||||
;; string bytes, so the log will show the readable hex address.
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x224) (i32.const 7) ;; "owner: "
|
||||
(i32.const 0x140) (local.get $ownerLen))
|
||||
)
|
||||
|
||||
;; ── transfer(name, new_owner) ─────────────────────────────────────────────
|
||||
;; Transfers ownership of `name` to `new_owner`.
|
||||
;; Only the current owner may call this (or anyone if name is unregistered).
|
||||
;; Logs "transferred: <name>" on success, "unauthorized: <name>" otherwise.
|
||||
(func (export "transfer")
|
||||
(local $nameLen i32)
|
||||
(local $newOwnerLen i32)
|
||||
(local $callerLen i32)
|
||||
(local $existingLen i32)
|
||||
|
||||
(local.set $nameLen
|
||||
(call $get_arg_str (i32.const 0) (i32.const 0x000) (i32.const 64)))
|
||||
(if (i32.eqz (local.get $nameLen)) (then return))
|
||||
|
||||
(local.set $newOwnerLen
|
||||
(call $get_arg_str (i32.const 1) (i32.const 0x040) (i32.const 128)))
|
||||
(if (i32.eqz (local.get $newOwnerLen)) (then return))
|
||||
|
||||
;; Read existing owner into 0x140
|
||||
(local.set $existingLen
|
||||
(call $get_state
|
||||
(i32.const 0x000) (local.get $nameLen)
|
||||
(i32.const 0x140) (i32.const 128)))
|
||||
|
||||
;; If not registered, anyone can claim → register directly for new_owner
|
||||
(if (i32.eqz (local.get $existingLen))
|
||||
(then
|
||||
(call $set_state
|
||||
(i32.const 0x000) (local.get $nameLen)
|
||||
(i32.const 0x040) (local.get $newOwnerLen))
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x22C) (i32.const 13) ;; "transferred: "
|
||||
(i32.const 0x000) (local.get $nameLen))
|
||||
return
|
||||
)
|
||||
)
|
||||
|
||||
;; Verify caller == existing owner
|
||||
(local.set $callerLen
|
||||
(call $get_caller (i32.const 0x0C0) (i32.const 128)))
|
||||
(if (i32.eqz
|
||||
(call $bytes_equal
|
||||
(i32.const 0x0C0) (i32.const 0x140)
|
||||
(if (result i32) (i32.ne (local.get $callerLen) (local.get $existingLen))
|
||||
(then (i32.const 0)) ;; length mismatch → not equal
|
||||
(else (local.get $callerLen)))))
|
||||
(then
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x23A) (i32.const 14) ;; "unauthorized: "
|
||||
(i32.const 0x000) (local.get $nameLen))
|
||||
return
|
||||
)
|
||||
)
|
||||
|
||||
;; Authorized — update owner
|
||||
(call $set_state
|
||||
(i32.const 0x000) (local.get $nameLen)
|
||||
(i32.const 0x040) (local.get $newOwnerLen))
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x22C) (i32.const 13) ;; "transferred: "
|
||||
(i32.const 0x000) (local.get $nameLen))
|
||||
)
|
||||
|
||||
;; ── release(name) ─────────────────────────────────────────────────────────
|
||||
;; Removes a name registration. Only the current owner may call this.
|
||||
;; Logs "released: <name>" on success.
|
||||
(func (export "release")
|
||||
(local $nameLen i32)
|
||||
(local $callerLen i32)
|
||||
(local $existingLen i32)
|
||||
|
||||
(local.set $nameLen
|
||||
(call $get_arg_str (i32.const 0) (i32.const 0x000) (i32.const 64)))
|
||||
(if (i32.eqz (local.get $nameLen)) (then return))
|
||||
|
||||
(local.set $existingLen
|
||||
(call $get_state
|
||||
(i32.const 0x000) (local.get $nameLen)
|
||||
(i32.const 0x140) (i32.const 128)))
|
||||
(if (i32.eqz (local.get $existingLen))
|
||||
(then
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x218) (i32.const 11) ;; "not found: "
|
||||
(i32.const 0x000) (local.get $nameLen))
|
||||
return
|
||||
)
|
||||
)
|
||||
|
||||
;; Verify caller == owner
|
||||
(local.set $callerLen
|
||||
(call $get_caller (i32.const 0x0C0) (i32.const 128)))
|
||||
(if (i32.eqz
|
||||
(call $bytes_equal
|
||||
(i32.const 0x0C0) (i32.const 0x140)
|
||||
(if (result i32) (i32.ne (local.get $callerLen) (local.get $existingLen))
|
||||
(then (i32.const 0))
|
||||
(else (local.get $callerLen)))))
|
||||
(then
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x23A) (i32.const 14) ;; "unauthorized: "
|
||||
(i32.const 0x000) (local.get $nameLen))
|
||||
return
|
||||
)
|
||||
)
|
||||
|
||||
;; Store empty bytes → effectively deletes the record
|
||||
(call $set_state
|
||||
(i32.const 0x000) (local.get $nameLen)
|
||||
(i32.const 0x000) (i32.const 0))
|
||||
(call $log_prefix_name
|
||||
(i32.const 0x249) (i32.const 10) ;; "released: "
|
||||
(i32.const 0x000) (local.get $nameLen))
|
||||
)
|
||||
)
|
||||
6
contracts/name_registry/name_registry_abi.json
Normal file
6
contracts/name_registry/name_registry_abi.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{"methods":[
|
||||
{"name":"register", "args":[{"name":"name","type":"string"}]},
|
||||
{"name":"resolve", "args":[{"name":"name","type":"string"}]},
|
||||
{"name":"transfer", "args":[{"name":"name","type":"string"},{"name":"new_owner","type":"string"}]},
|
||||
{"name":"release", "args":[{"name":"name","type":"string"}]}
|
||||
]}
|
||||
210
contracts/sdk/dchain.go
Normal file
210
contracts/sdk/dchain.go
Normal file
@@ -0,0 +1,210 @@
|
||||
//go:build tinygo
|
||||
|
||||
// Package dchain is the DChain smart contract SDK for TinyGo.
|
||||
//
|
||||
// # Build a contract
|
||||
//
|
||||
// tinygo build -o mycontract.wasm -target wasip1 -no-debug ./mycontract
|
||||
//
|
||||
// # Deploy
|
||||
//
|
||||
// client deploy-contract --key key.json \
|
||||
// --wasm mycontract.wasm --abi mycontract_abi.json \
|
||||
// --node http://localhost:8081
|
||||
//
|
||||
// Each exported Go function becomes a callable contract method.
|
||||
// All inputs come through Arg*/GetState; all outputs go through Log/SetState.
|
||||
package dchain
|
||||
|
||||
import "unsafe"
|
||||
|
||||
// ── Argument accessors ────────────────────────────────────────────────────────
|
||||
|
||||
//go:wasmimport env get_arg_str
|
||||
func hostGetArgStr(idx uint32, ptr uintptr, maxLen uint32) uint32
|
||||
|
||||
//go:wasmimport env get_arg_u64
|
||||
func hostGetArgU64(idx uint32) uint64
|
||||
|
||||
// ArgStr returns the idx-th call argument as a string (max maxLen bytes).
|
||||
// Returns "" if the index is out of range.
|
||||
func ArgStr(idx int, maxLen int) string {
|
||||
buf := make([]byte, maxLen)
|
||||
n := hostGetArgStr(uint32(idx), uintptr(unsafe.Pointer(&buf[0])), uint32(maxLen))
|
||||
if n == 0 {
|
||||
return ""
|
||||
}
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
// ArgU64 returns the idx-th call argument as a uint64. Returns 0 if out of range.
|
||||
func ArgU64(idx int) uint64 {
|
||||
return hostGetArgU64(uint32(idx))
|
||||
}
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
//go:wasmimport env get_state
|
||||
func hostGetState(kPtr uintptr, kLen uint32, dstPtr uintptr, dstLen uint32) uint32
|
||||
|
||||
//go:wasmimport env set_state
|
||||
func hostSetState(kPtr uintptr, kLen uint32, vPtr uintptr, vLen uint32)
|
||||
|
||||
//go:wasmimport env get_u64
|
||||
func hostGetU64(kPtr uintptr, kLen uint32) uint64
|
||||
|
||||
//go:wasmimport env put_u64
|
||||
func hostPutU64(kPtr uintptr, kLen uint32, val uint64)
|
||||
|
||||
// GetState reads a value from contract state by key.
|
||||
// Returns nil if the key does not exist.
|
||||
func GetState(key string) []byte {
|
||||
k := []byte(key)
|
||||
buf := make([]byte, 1024)
|
||||
n := hostGetState(
|
||||
uintptr(unsafe.Pointer(&k[0])), uint32(len(k)),
|
||||
uintptr(unsafe.Pointer(&buf[0])), uint32(len(buf)),
|
||||
)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
return buf[:n]
|
||||
}
|
||||
|
||||
// GetStateStr reads a contract state value as a string.
|
||||
func GetStateStr(key string) string {
|
||||
v := GetState(key)
|
||||
if v == nil {
|
||||
return ""
|
||||
}
|
||||
return string(v)
|
||||
}
|
||||
|
||||
// SetState writes a value to contract state.
|
||||
// Passing an empty slice clears the key.
|
||||
func SetState(key string, value []byte) {
|
||||
k := []byte(key)
|
||||
if len(value) == 0 {
|
||||
// vPtr=0, vLen=0 → clear key
|
||||
hostSetState(uintptr(unsafe.Pointer(&k[0])), uint32(len(k)), 0, 0)
|
||||
return
|
||||
}
|
||||
hostSetState(
|
||||
uintptr(unsafe.Pointer(&k[0])), uint32(len(k)),
|
||||
uintptr(unsafe.Pointer(&value[0])), uint32(len(value)),
|
||||
)
|
||||
}
|
||||
|
||||
// SetStateStr writes a string value to contract state.
|
||||
func SetStateStr(key, value string) {
|
||||
SetState(key, []byte(value))
|
||||
}
|
||||
|
||||
// GetU64 reads a uint64 stored by PutU64 from contract state.
|
||||
func GetU64(key string) uint64 {
|
||||
k := []byte(key)
|
||||
return hostGetU64(uintptr(unsafe.Pointer(&k[0])), uint32(len(k)))
|
||||
}
|
||||
|
||||
// PutU64 stores a uint64 in contract state as 8-byte big-endian.
|
||||
func PutU64(key string, val uint64) {
|
||||
k := []byte(key)
|
||||
hostPutU64(uintptr(unsafe.Pointer(&k[0])), uint32(len(k)), val)
|
||||
}
|
||||
|
||||
// ── Caller & chain ────────────────────────────────────────────────────────────
|
||||
|
||||
//go:wasmimport env get_caller
|
||||
func hostGetCaller(bufPtr uintptr, bufLen uint32) uint32
|
||||
|
||||
//go:wasmimport env get_block_height
|
||||
func hostGetBlockHeight() uint64
|
||||
|
||||
//go:wasmimport env get_contract_treasury
|
||||
func hostGetContractTreasury(bufPtr uintptr, bufLen uint32) uint32
|
||||
|
||||
// Caller returns the hex pubkey of the transaction sender (or parent contract ID).
|
||||
func Caller() string {
|
||||
buf := make([]byte, 128)
|
||||
n := hostGetCaller(uintptr(unsafe.Pointer(&buf[0])), uint32(len(buf)))
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
// BlockHeight returns the height of the block currently being processed.
|
||||
func BlockHeight() uint64 {
|
||||
return hostGetBlockHeight()
|
||||
}
|
||||
|
||||
// Treasury returns the contract's ownerless escrow address.
|
||||
// Derived as hex(sha256(contractID+":treasury")); no private key exists.
|
||||
// Only this contract can spend from it via Transfer.
|
||||
func Treasury() string {
|
||||
buf := make([]byte, 64)
|
||||
n := hostGetContractTreasury(uintptr(unsafe.Pointer(&buf[0])), uint32(len(buf)))
|
||||
return string(buf[:n])
|
||||
}
|
||||
|
||||
// ── Token operations ──────────────────────────────────────────────────────────
|
||||
|
||||
//go:wasmimport env get_balance
|
||||
func hostGetBalance(pubPtr uintptr, pubLen uint32) int64
|
||||
|
||||
//go:wasmimport env transfer
|
||||
func hostTransfer(fromPtr uintptr, fromLen uint32, toPtr uintptr, toLen uint32, amount uint64) uint32
|
||||
|
||||
// Balance returns the token balance of a hex pubkey address in µT.
|
||||
func Balance(pubKey string) uint64 {
|
||||
p := []byte(pubKey)
|
||||
return uint64(hostGetBalance(uintptr(unsafe.Pointer(&p[0])), uint32(len(p))))
|
||||
}
|
||||
|
||||
// Transfer sends amount µT from one address to another.
|
||||
// Returns true on success, false if from has insufficient balance.
|
||||
func Transfer(from, to string, amount uint64) bool {
|
||||
f := []byte(from)
|
||||
t := []byte(to)
|
||||
return hostTransfer(
|
||||
uintptr(unsafe.Pointer(&f[0])), uint32(len(f)),
|
||||
uintptr(unsafe.Pointer(&t[0])), uint32(len(t)),
|
||||
amount,
|
||||
) == 0
|
||||
}
|
||||
|
||||
// ── Inter-contract calls ──────────────────────────────────────────────────────
|
||||
|
||||
//go:wasmimport env call_contract
|
||||
func hostCallContract(cidPtr uintptr, cidLen uint32, mthPtr uintptr, mthLen uint32, argPtr uintptr, argLen uint32) uint32
|
||||
|
||||
// CallContract executes a method on another deployed contract.
|
||||
// argsJSON must be a JSON array, e.g. `["alice", "100"]`.
|
||||
// Caller of the sub-contract is set to this contract's ID.
|
||||
// Gas is shared — sub-call consumes from the parent's gas budget.
|
||||
// Returns true on success.
|
||||
func CallContract(contractID, method, argsJSON string) bool {
|
||||
cid := []byte(contractID)
|
||||
mth := []byte(method)
|
||||
if argsJSON == "" {
|
||||
argsJSON = "[]"
|
||||
}
|
||||
arg := []byte(argsJSON)
|
||||
return hostCallContract(
|
||||
uintptr(unsafe.Pointer(&cid[0])), uint32(len(cid)),
|
||||
uintptr(unsafe.Pointer(&mth[0])), uint32(len(mth)),
|
||||
uintptr(unsafe.Pointer(&arg[0])), uint32(len(arg)),
|
||||
) == 0
|
||||
}
|
||||
|
||||
// ── Logging ───────────────────────────────────────────────────────────────────
|
||||
|
||||
//go:wasmimport env log
|
||||
func hostLog(msgPtr uintptr, msgLen uint32)
|
||||
|
||||
// Log writes a message to the contract log.
|
||||
// Logs are visible in the block explorer at /contract?id=<id> → Logs tab.
|
||||
func Log(msg string) {
|
||||
b := []byte(msg)
|
||||
if len(b) == 0 {
|
||||
return
|
||||
}
|
||||
hostLog(uintptr(unsafe.Pointer(&b[0])), uint32(len(b)))
|
||||
}
|
||||
53
contracts/sdk/dchain_stub.go
Normal file
53
contracts/sdk/dchain_stub.go
Normal file
@@ -0,0 +1,53 @@
|
||||
//go:build !tinygo
|
||||
|
||||
// Package dchain provides stub implementations for non-TinyGo builds.
|
||||
// These allow go build / IDEs to compile contract code without TinyGo.
|
||||
// The stubs panic at runtime — they are never executed in production.
|
||||
package dchain
|
||||
|
||||
// ArgStr returns the idx-th call argument as a string.
|
||||
func ArgStr(idx int, maxLen int) string { panic("dchain: ArgStr requires TinyGo (tinygo build -target wasip1)") }
|
||||
|
||||
// ArgU64 returns the idx-th call argument as a uint64.
|
||||
func ArgU64(idx int) uint64 { panic("dchain: ArgU64 requires TinyGo") }
|
||||
|
||||
// GetState reads a value from contract state.
|
||||
func GetState(key string) []byte { panic("dchain: GetState requires TinyGo") }
|
||||
|
||||
// GetStateStr reads a contract state value as a string.
|
||||
func GetStateStr(key string) string { panic("dchain: GetStateStr requires TinyGo") }
|
||||
|
||||
// SetState writes a value to contract state.
|
||||
func SetState(key string, value []byte) { panic("dchain: SetState requires TinyGo") }
|
||||
|
||||
// SetStateStr writes a string value to contract state.
|
||||
func SetStateStr(key, value string) { panic("dchain: SetStateStr requires TinyGo") }
|
||||
|
||||
// GetU64 reads a uint64 from contract state.
|
||||
func GetU64(key string) uint64 { panic("dchain: GetU64 requires TinyGo") }
|
||||
|
||||
// PutU64 stores a uint64 in contract state.
|
||||
func PutU64(key string, val uint64) { panic("dchain: PutU64 requires TinyGo") }
|
||||
|
||||
// Caller returns the hex pubkey of the transaction sender.
|
||||
func Caller() string { panic("dchain: Caller requires TinyGo") }
|
||||
|
||||
// BlockHeight returns the current block height.
|
||||
func BlockHeight() uint64 { panic("dchain: BlockHeight requires TinyGo") }
|
||||
|
||||
// Treasury returns the contract's ownerless treasury address.
|
||||
func Treasury() string { panic("dchain: Treasury requires TinyGo") }
|
||||
|
||||
// Balance returns the token balance of a hex pubkey in µT.
|
||||
func Balance(pubKey string) uint64 { panic("dchain: Balance requires TinyGo") }
|
||||
|
||||
// Transfer sends amount µT from one address to another.
|
||||
func Transfer(from, to string, amount uint64) bool { panic("dchain: Transfer requires TinyGo") }
|
||||
|
||||
// CallContract executes a method on another deployed contract.
|
||||
func CallContract(contractID, method, argsJSON string) bool {
|
||||
panic("dchain: CallContract requires TinyGo")
|
||||
}
|
||||
|
||||
// Log writes a message to the contract log.
|
||||
func Log(msg string) { panic("dchain: Log requires TinyGo") }
|
||||
678
contracts/username_registry/gen/main.go
Normal file
678
contracts/username_registry/gen/main.go
Normal file
@@ -0,0 +1,678 @@
|
||||
// gen generates contracts/username_registry/username_registry.wasm
|
||||
// Run from repo root: go run ./contracts/username_registry/gen/
|
||||
//
|
||||
// Methods: register, resolve, lookup, transfer, release, fee
|
||||
//
|
||||
// State layout:
|
||||
// "name:<name>" → owner address bytes
|
||||
// "addr:<address>" → name bytes
|
||||
//
|
||||
// Fee schedule (µT):
|
||||
// len=1 → 10_000_000 len=2 → 1_000_000 len=3 → 100_000
|
||||
// len=4 → 10_000 len=5 → 1_000 len≥6 → 100
|
||||
//
|
||||
// Fees are transferred from caller to the contract treasury address
|
||||
// (sha256(contractID+":treasury") — ownerless, only the contract can spend).
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// ── LEB128 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func u(v uint64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
if v != 0 {
|
||||
bt |= 0x80
|
||||
}
|
||||
b = append(b, bt)
|
||||
if v == 0 {
|
||||
return b
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func s(v int64) []byte {
|
||||
var b []byte
|
||||
for {
|
||||
bt := byte(v & 0x7f)
|
||||
v >>= 7
|
||||
sign := (bt & 0x40) != 0
|
||||
if (v == 0 && !sign) || (v == -1 && sign) {
|
||||
return append(b, bt)
|
||||
}
|
||||
b = append(b, bt|0x80)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Builders ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func cat(slices ...[]byte) []byte {
|
||||
var out []byte
|
||||
for _, sl := range slices {
|
||||
out = append(out, sl...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func wstr(str string) []byte { return cat(u(uint64(len(str))), []byte(str)) }
|
||||
|
||||
func section(id byte, content []byte) []byte {
|
||||
return cat([]byte{id}, u(uint64(len(content))), content)
|
||||
}
|
||||
|
||||
func vec(items ...[]byte) []byte {
|
||||
out := u(uint64(len(items)))
|
||||
for _, it := range items {
|
||||
out = append(out, it...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func functype(params, results []byte) []byte {
|
||||
return cat([]byte{0x60}, u(uint64(len(params))), params, u(uint64(len(results))), results)
|
||||
}
|
||||
|
||||
func importFunc(mod, name string, typeIdx uint32) []byte {
|
||||
return cat(wstr(mod), wstr(name), []byte{0x00}, u(uint64(typeIdx)))
|
||||
}
|
||||
|
||||
func exportEntry(name string, kind byte, idx uint32) []byte {
|
||||
return cat(wstr(name), []byte{kind}, u(uint64(idx)))
|
||||
}
|
||||
|
||||
func dataSegment(offset int32, data []byte) []byte {
|
||||
return cat(
|
||||
[]byte{0x00},
|
||||
[]byte{0x41}, s(int64(offset)), []byte{0x0B},
|
||||
u(uint64(len(data))), data,
|
||||
)
|
||||
}
|
||||
|
||||
func funcBody(localDecls []byte, instrs ...[]byte) []byte {
|
||||
inner := cat(localDecls)
|
||||
for _, ins := range instrs {
|
||||
inner = append(inner, ins...)
|
||||
}
|
||||
inner = append(inner, 0x0B) // end
|
||||
return cat(u(uint64(len(inner))), inner)
|
||||
}
|
||||
|
||||
var noLocals = u(0)
|
||||
|
||||
func localDecl(n uint32, typ byte) []byte { return cat(u(uint64(n)), []byte{typ}) }
|
||||
func withLocals(decls ...[]byte) []byte {
|
||||
return cat(u(uint64(len(decls))), cat(decls...))
|
||||
}
|
||||
|
||||
// ── Instructions ──────────────────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
tI32 byte = 0x7F
|
||||
tI64 byte = 0x7E
|
||||
)
|
||||
|
||||
func call(fn uint32) []byte { return cat([]byte{0x10}, u(uint64(fn))) }
|
||||
func lget(i uint32) []byte { return cat([]byte{0x20}, u(uint64(i))) }
|
||||
func lset(i uint32) []byte { return cat([]byte{0x21}, u(uint64(i))) }
|
||||
func ic32(v int32) []byte { return cat([]byte{0x41}, s(int64(v))) }
|
||||
func ic64(v int64) []byte { return cat([]byte{0x42}, s(v)) }
|
||||
func block_() []byte { return []byte{0x02, 0x40} }
|
||||
func loop_() []byte { return []byte{0x03, 0x40} }
|
||||
func if_() []byte { return []byte{0x04, 0x40} }
|
||||
func ifI32() []byte { return []byte{0x04, tI32} }
|
||||
func else_() []byte { return []byte{0x05} }
|
||||
func end_() []byte { return []byte{0x0B} }
|
||||
func br_(lbl uint32) []byte { return cat([]byte{0x0C}, u(uint64(lbl))) }
|
||||
func brIf_(lbl uint32) []byte { return cat([]byte{0x0D}, u(uint64(lbl))) }
|
||||
func return_() []byte { return []byte{0x0F} }
|
||||
func drop() []byte { return []byte{0x1A} }
|
||||
func i32Eqz() []byte { return []byte{0x45} }
|
||||
func i32Ne() []byte { return []byte{0x47} }
|
||||
func i32GtU() []byte { return []byte{0x4B} }
|
||||
func i32GeU() []byte { return []byte{0x4F} }
|
||||
func i32LeU() []byte { return []byte{0x4D} }
|
||||
func i32Add() []byte { return []byte{0x6A} }
|
||||
func i64LtU() []byte { return []byte{0x54} }
|
||||
func i64Eqz() []byte { return []byte{0x50} }
|
||||
func i32Load8U() []byte { return []byte{0x2D, 0x00, 0x00} }
|
||||
func i32Store8() []byte { return []byte{0x3A, 0x00, 0x00} }
|
||||
|
||||
// ── Memory layout ─────────────────────────────────────────────────────────────
|
||||
//
|
||||
// 0x000 64 arg[0] name buffer
|
||||
// 0x040 128 arg[1] new_owner buffer
|
||||
// 0x0C0 128 caller buffer
|
||||
// 0x140 64 contract treasury buffer
|
||||
// 0x180 128 state-read result buffer
|
||||
//
|
||||
// Constant string data (read-only, written by data segments):
|
||||
// 0x200 5 "name:"
|
||||
// 0x206 5 "addr:"
|
||||
// 0x20C 12 "registered: "
|
||||
// 0x218 12 "name taken: "
|
||||
// 0x224 11 "not found: "
|
||||
// 0x230 7 "owner: "
|
||||
// 0x238 13 "transferred: "
|
||||
// 0x246 14 "unauthorized: "
|
||||
// 0x255 10 "released: "
|
||||
// 0x260 6 "name: "
|
||||
// 0x267 9 "no name: "
|
||||
// 0x271 14 "insufficient: "
|
||||
// 0x280 13 "fee: 10000000"
|
||||
// 0x28D 12 "fee: 1000000"
|
||||
// 0x299 11 "fee: 100000"
|
||||
// 0x2A4 10 "fee: 10000"
|
||||
// 0x2AE 9 "fee: 1000"
|
||||
// 0x2B7 8 "fee: 100"
|
||||
//
|
||||
// 0x300 256 scratch buffer (used by buildKey + logPrefixName)
|
||||
|
||||
const (
|
||||
offArg0 int32 = 0x000
|
||||
offArg1 int32 = 0x040
|
||||
offCaller int32 = 0x0C0
|
||||
offTreasury int32 = 0x140
|
||||
offStateRead int32 = 0x180
|
||||
|
||||
offPfxName int32 = 0x200 // "name:" 5 bytes
|
||||
offPfxAddr int32 = 0x206 // "addr:" 5 bytes
|
||||
|
||||
offRegisteredPfx int32 = 0x20C // "registered: " 12
|
||||
offNameTakenPfx int32 = 0x218 // "name taken: " 12
|
||||
offNotFoundPfx int32 = 0x224 // "not found: " 11
|
||||
offOwnerPfx int32 = 0x230 // "owner: " 7
|
||||
offTransferredPfx int32 = 0x238 // "transferred: " 13
|
||||
offUnauthPfx int32 = 0x246 // "unauthorized: " 14
|
||||
offReleasedPfx int32 = 0x255 // "released: " 10
|
||||
offNamePfx int32 = 0x260 // "name: " 6
|
||||
offNoNamePfx int32 = 0x267 // "no name: " 9
|
||||
offInsuffPfx int32 = 0x271 // "insufficient: " 14
|
||||
|
||||
offFee1 int32 = 0x280 // "fee: 10000000" 13
|
||||
offFee2 int32 = 0x28D // "fee: 1000000" 12
|
||||
offFee3 int32 = 0x299 // "fee: 100000" 11
|
||||
offFee4 int32 = 0x2A4 // "fee: 10000" 10
|
||||
offFee5 int32 = 0x2AE // "fee: 1000" 9
|
||||
offFee6 int32 = 0x2B7 // "fee: 100" 8
|
||||
|
||||
offScratch int32 = 0x300
|
||||
)
|
||||
|
||||
// ── Import / function indices ─────────────────────────────────────────────────
|
||||
|
||||
const (
|
||||
// imports (0-based)
|
||||
fnGetArgStr = 0
|
||||
fnGetCaller = 1
|
||||
fnGetState = 2
|
||||
fnSetState = 3
|
||||
fnLog = 4
|
||||
fnTransfer = 5
|
||||
fnGetBalance = 6
|
||||
fnGetContractTreasury = 7
|
||||
|
||||
// local functions (after 8 imports)
|
||||
fnBytesEqual = 8
|
||||
fnMemcpy = 9
|
||||
fnLogPrefixName = 10
|
||||
fnCalcFee = 11
|
||||
fnBuildKey = 12
|
||||
fnRegister = 13
|
||||
fnResolve = 14
|
||||
fnLookup = 15
|
||||
fnTransferFn = 16
|
||||
fnRelease = 17
|
||||
fnFee = 18
|
||||
)
|
||||
|
||||
// ── Helper function bodies ────────────────────────────────────────────────────
|
||||
|
||||
// $bytes_equal(aPtr, bPtr, len i32) → i32 (1=equal, 0=not)
|
||||
// extra locals: i(3), same(4)
|
||||
func bytesEqualBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(2, tI32)),
|
||||
ic32(1), lset(4),
|
||||
ic32(0), lset(3),
|
||||
block_(),
|
||||
loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1),
|
||||
lget(0), lget(3), i32Add(), i32Load8U(),
|
||||
lget(1), lget(3), i32Add(), i32Load8U(),
|
||||
i32Ne(), if_(),
|
||||
ic32(0), lset(4), br_(2),
|
||||
end_(),
|
||||
lget(3), ic32(1), i32Add(), lset(3),
|
||||
br_(0),
|
||||
end_(),
|
||||
end_(),
|
||||
lget(4),
|
||||
)
|
||||
}
|
||||
|
||||
// $memcpy(dst, src, len i32)
|
||||
// extra local: i(3)
|
||||
func memcpyBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(1, tI32)),
|
||||
ic32(0), lset(3),
|
||||
block_(),
|
||||
loop_(),
|
||||
lget(3), lget(2), i32GeU(), brIf_(1),
|
||||
lget(0), lget(3), i32Add(),
|
||||
lget(1), lget(3), i32Add(), i32Load8U(),
|
||||
i32Store8(),
|
||||
lget(3), ic32(1), i32Add(), lset(3),
|
||||
br_(0),
|
||||
end_(),
|
||||
end_(),
|
||||
)
|
||||
}
|
||||
|
||||
// $log_prefix_name(prefixPtr, prefixLen, suffixPtr, suffixLen i32)
|
||||
// Concatenates prefix+suffix into scratch and logs the result.
|
||||
func logPrefixNameBody() []byte {
|
||||
return funcBody(
|
||||
noLocals,
|
||||
ic32(offScratch), lget(0), lget(1), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), i32Add(), lget(2), lget(3), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), lget(3), i32Add(), call(fnLog),
|
||||
)
|
||||
}
|
||||
|
||||
// $calc_fee(nameLen i32) → i64
|
||||
// Fee tiers: 1→10M, 2→1M, 3→100K, 4→10K, 5→1K, ≥6→100 µT
|
||||
func calcFeeBody() []byte {
|
||||
return funcBody(
|
||||
noLocals,
|
||||
lget(0), ic32(1), i32LeU(), if_(),
|
||||
ic64(10_000_000), return_(),
|
||||
end_(),
|
||||
lget(0), ic32(2), i32Ne(), if_(), // len>=3
|
||||
lget(0), ic32(3), i32Ne(), if_(), // len>=4
|
||||
lget(0), ic32(4), i32Ne(), if_(), // len>=5
|
||||
lget(0), ic32(5), i32Ne(), if_(), // len>=6
|
||||
ic64(100), return_(),
|
||||
end_(),
|
||||
ic64(1_000), return_(),
|
||||
end_(),
|
||||
ic64(10_000), return_(),
|
||||
end_(),
|
||||
ic64(100_000), return_(),
|
||||
end_(),
|
||||
ic64(1_000_000),
|
||||
)
|
||||
}
|
||||
|
||||
// $build_key(pfxOff, pfxLen, dataOff, dataLen i32) → keyLen i32
|
||||
// Writes pfx+data into scratch (offScratch) and returns total length.
|
||||
func buildKeyBody() []byte {
|
||||
return funcBody(
|
||||
noLocals,
|
||||
ic32(offScratch), lget(0), lget(1), call(fnMemcpy),
|
||||
ic32(offScratch), lget(1), i32Add(), lget(2), lget(3), call(fnMemcpy),
|
||||
lget(1), lget(3), i32Add(),
|
||||
)
|
||||
}
|
||||
|
||||
// isOwnerCheck emits code that, given two length locals, leaves i32(1=owner,0=not) on stack.
|
||||
// Assumes caller bytes are at offCaller and existing owner bytes at offStateRead.
|
||||
func isOwnerCheck(callerLenLocal, existingLenLocal uint32) []byte {
|
||||
return cat(
|
||||
lget(callerLenLocal), lget(existingLenLocal), i32Ne(),
|
||||
ifI32(),
|
||||
ic32(0),
|
||||
else_(),
|
||||
ic32(offCaller), ic32(offStateRead), lget(callerLenLocal), call(fnBytesEqual),
|
||||
end_(),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Contract method bodies ────────────────────────────────────────────────────
|
||||
|
||||
// register(name string)
|
||||
// Locals (i32): nameLen(0), callerLen(1), existingLen(2), treasuryLen(3), keyLen(4)
|
||||
// Locals (i64): fee(5)
|
||||
func registerBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(5, tI32), localDecl(1, tI64)),
|
||||
|
||||
// Read name arg
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Check name not already taken
|
||||
ic32(offPfxName), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offStateRead), ic32(128), call(fnGetState), lset(2),
|
||||
lget(2), ic32(0), i32GtU(), if_(),
|
||||
ic32(offNameTakenPfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get caller
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
|
||||
// Compute fee
|
||||
lget(0), call(fnCalcFee), lset(5),
|
||||
|
||||
// Check balance >= fee
|
||||
ic32(offCaller), lget(1), call(fnGetBalance),
|
||||
lget(5), i64LtU(), if_(),
|
||||
ic32(offInsuffPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Get treasury address
|
||||
ic32(offTreasury), ic32(64), call(fnGetContractTreasury), lset(3),
|
||||
|
||||
// Transfer fee: caller → treasury
|
||||
ic32(offCaller), lget(1), ic32(offTreasury), lget(3), lget(5), call(fnTransfer), drop(),
|
||||
|
||||
// Write name→owner
|
||||
ic32(offPfxName), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offCaller), lget(1), call(fnSetState),
|
||||
|
||||
// Write addr→name (reverse index)
|
||||
ic32(offPfxAddr), ic32(5), ic32(offCaller), lget(1), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offArg0), lget(0), call(fnSetState),
|
||||
|
||||
// Log success
|
||||
ic32(offRegisteredPfx), ic32(12), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// resolve(name string)
|
||||
// Locals: nameLen(0), ownerLen(1), keyLen(2)
|
||||
func resolveBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(3, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
ic32(offPfxName), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(2),
|
||||
ic32(offScratch), lget(2), ic32(offStateRead), ic32(128), call(fnGetState), lset(1),
|
||||
lget(1), i32Eqz(), if_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offOwnerPfx), ic32(7), ic32(offStateRead), lget(1), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// lookup(address string) — reverse lookup address → name
|
||||
// Locals: addrLen(0), nameLen(1), keyLen(2)
|
||||
func lookupBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(3, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(128), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
ic32(offPfxAddr), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(2),
|
||||
ic32(offScratch), lget(2), ic32(offStateRead), ic32(128), call(fnGetState), lset(1),
|
||||
lget(1), i32Eqz(), if_(),
|
||||
ic32(offNoNamePfx), ic32(9), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
ic32(offNamePfx), ic32(6), ic32(offStateRead), lget(1), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// transfer(name, new_owner) — transfer username ownership
|
||||
// Locals: nameLen(0), newOwnerLen(1), callerLen(2), existingLen(3), keyLen(4)
|
||||
func transferBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(5, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
ic32(1), ic32(offArg1), ic32(128), call(fnGetArgStr), lset(1),
|
||||
lget(1), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Look up existing owner
|
||||
ic32(offPfxName), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offStateRead), ic32(128), call(fnGetState), lset(3),
|
||||
lget(3), i32Eqz(), if_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Verify caller is the current owner
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(2),
|
||||
isOwnerCheck(2, 3),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Delete old reverse index: addr:<oldOwner> → ""
|
||||
ic32(offPfxAddr), ic32(5), ic32(offStateRead), lget(3), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offScratch), ic32(0), call(fnSetState),
|
||||
|
||||
// Update forward index: name:<name> → newOwner
|
||||
ic32(offPfxName), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offArg1), lget(1), call(fnSetState),
|
||||
|
||||
// Add new reverse index: addr:<newOwner> → name
|
||||
ic32(offPfxAddr), ic32(5), ic32(offArg1), lget(1), call(fnBuildKey), lset(4),
|
||||
ic32(offScratch), lget(4), ic32(offArg0), lget(0), call(fnSetState),
|
||||
|
||||
ic32(offTransferredPfx), ic32(13), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// release(name) — release a username registration
|
||||
// Locals: nameLen(0), callerLen(1), existingLen(2), keyLen(3)
|
||||
func releaseBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(4, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
ic32(offPfxName), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offStateRead), ic32(128), call(fnGetState), lset(2),
|
||||
lget(2), i32Eqz(), if_(),
|
||||
ic32(offNotFoundPfx), ic32(11), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
ic32(offCaller), ic32(128), call(fnGetCaller), lset(1),
|
||||
isOwnerCheck(1, 2),
|
||||
i32Eqz(), if_(),
|
||||
ic32(offUnauthPfx), ic32(14), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
return_(),
|
||||
end_(),
|
||||
|
||||
// Delete reverse index: addr:<owner> → ""
|
||||
ic32(offPfxAddr), ic32(5), ic32(offStateRead), lget(2), call(fnBuildKey), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offScratch), ic32(0), call(fnSetState),
|
||||
|
||||
// Delete forward index: name:<name> → ""
|
||||
ic32(offPfxName), ic32(5), ic32(offArg0), lget(0), call(fnBuildKey), lset(3),
|
||||
ic32(offScratch), lget(3), ic32(offScratch), ic32(0), call(fnSetState),
|
||||
|
||||
ic32(offReleasedPfx), ic32(10), ic32(offArg0), lget(0), call(fnLogPrefixName),
|
||||
)
|
||||
}
|
||||
|
||||
// fee(name) — log the registration fee for a given name
|
||||
// Uses pre-baked static strings; no arithmetic needed.
|
||||
// Locals: nameLen(0)
|
||||
func feeBody() []byte {
|
||||
return funcBody(
|
||||
withLocals(localDecl(1, tI32)),
|
||||
ic32(0), ic32(offArg0), ic32(64), call(fnGetArgStr), lset(0),
|
||||
lget(0), i32Eqz(), if_(), return_(), end_(),
|
||||
|
||||
// Branch on name length and log the matching static fee string.
|
||||
lget(0), ic32(1), i32LeU(), if_(),
|
||||
ic32(offFee1), ic32(13), call(fnLog), return_(),
|
||||
end_(),
|
||||
lget(0), ic32(2), i32Ne(), if_(), // len >= 3
|
||||
lget(0), ic32(3), i32Ne(), if_(), // len >= 4
|
||||
lget(0), ic32(4), i32Ne(), if_(), // len >= 5
|
||||
lget(0), ic32(5), i32Ne(), if_(), // len >= 6
|
||||
ic32(offFee6), ic32(8), call(fnLog), return_(),
|
||||
end_(),
|
||||
ic32(offFee5), ic32(9), call(fnLog), return_(),
|
||||
end_(),
|
||||
ic32(offFee4), ic32(10), call(fnLog), return_(),
|
||||
end_(),
|
||||
ic32(offFee3), ic32(11), call(fnLog), return_(),
|
||||
end_(),
|
||||
// len == 2
|
||||
ic32(offFee2), ic32(12), call(fnLog),
|
||||
)
|
||||
}
|
||||
|
||||
// ── main ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
func main() {
|
||||
// ── Type table ────────────────────────────────────────────────────────────
|
||||
// 0: (i32,i32,i32)→(i32) get_arg_str, bytes_equal
|
||||
// 1: (i32,i32)→(i32) get_caller, get_contract_treasury
|
||||
// 2: (i32,i32,i32,i32)→(i32) get_state, build_key
|
||||
// 3: (i32,i32,i32,i32)→() set_state, log_prefix_name
|
||||
// 4: (i32,i32)→() log
|
||||
// 5: (i32,i32,i32,i32,i64)→(i32) transfer
|
||||
// 6: (i32,i32)→(i64) get_balance
|
||||
// 7: ()→() exported methods
|
||||
// 8: (i32,i32,i32)→() memcpy
|
||||
// 9: (i32)→(i64) calc_fee
|
||||
typeSection := section(0x01, vec(
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{tI32}), // 0
|
||||
functype([]byte{tI32, tI32}, []byte{tI32}), // 1
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{tI32}), // 2
|
||||
functype([]byte{tI32, tI32, tI32, tI32}, []byte{}), // 3
|
||||
functype([]byte{tI32, tI32}, []byte{}), // 4
|
||||
functype([]byte{tI32, tI32, tI32, tI32, tI64}, []byte{tI32}), // 5
|
||||
functype([]byte{tI32, tI32}, []byte{tI64}), // 6
|
||||
functype([]byte{}, []byte{}), // 7
|
||||
functype([]byte{tI32, tI32, tI32}, []byte{}), // 8
|
||||
functype([]byte{tI32}, []byte{tI64}), // 9
|
||||
))
|
||||
|
||||
importSection := section(0x02, vec(
|
||||
importFunc("env", "get_arg_str", 0), // 0 type 0
|
||||
importFunc("env", "get_caller", 1), // 1 type 1
|
||||
importFunc("env", "get_state", 2), // 2 type 2
|
||||
importFunc("env", "set_state", 3), // 3 type 3
|
||||
importFunc("env", "log", 4), // 4 type 4
|
||||
importFunc("env", "transfer", 5), // 5 type 5
|
||||
importFunc("env", "get_balance", 6), // 6 type 6
|
||||
importFunc("env", "get_contract_treasury", 1), // 7 type 1
|
||||
))
|
||||
|
||||
// 11 local functions
|
||||
functionSection := section(0x03, vec(
|
||||
u(0), // bytes_equal type 0
|
||||
u(8), // memcpy type 8
|
||||
u(3), // log_prefix_name type 3
|
||||
u(9), // calc_fee type 9
|
||||
u(2), // build_key type 2
|
||||
u(7), // register type 7
|
||||
u(7), // resolve type 7
|
||||
u(7), // lookup type 7
|
||||
u(7), // transfer_fn type 7
|
||||
u(7), // release type 7
|
||||
u(7), // fee type 7
|
||||
))
|
||||
|
||||
memorySection := section(0x05, vec(cat([]byte{0x00}, u(1))))
|
||||
|
||||
exportSection := section(0x07, vec(
|
||||
exportEntry("memory", 0x02, 0),
|
||||
exportEntry("register", 0x00, fnRegister),
|
||||
exportEntry("resolve", 0x00, fnResolve),
|
||||
exportEntry("lookup", 0x00, fnLookup),
|
||||
exportEntry("transfer", 0x00, fnTransferFn),
|
||||
exportEntry("release", 0x00, fnRelease),
|
||||
exportEntry("fee", 0x00, fnFee),
|
||||
))
|
||||
|
||||
dataSection := section(0x0B, cat(
|
||||
u(19),
|
||||
dataSegment(offPfxName, []byte("name:")),
|
||||
dataSegment(offPfxAddr, []byte("addr:")),
|
||||
dataSegment(offRegisteredPfx, []byte("registered: ")),
|
||||
dataSegment(offNameTakenPfx, []byte("name taken: ")),
|
||||
dataSegment(offNotFoundPfx, []byte("not found: ")),
|
||||
dataSegment(offOwnerPfx, []byte("owner: ")),
|
||||
dataSegment(offTransferredPfx, []byte("transferred: ")),
|
||||
dataSegment(offUnauthPfx, []byte("unauthorized: ")),
|
||||
dataSegment(offReleasedPfx, []byte("released: ")),
|
||||
dataSegment(offNamePfx, []byte("name: ")),
|
||||
dataSegment(offNoNamePfx, []byte("no name: ")),
|
||||
dataSegment(offInsuffPfx, []byte("insufficient: ")),
|
||||
dataSegment(offFee1, []byte("fee: 10000000")),
|
||||
dataSegment(offFee2, []byte("fee: 1000000")),
|
||||
dataSegment(offFee3, []byte("fee: 100000")),
|
||||
dataSegment(offFee4, []byte("fee: 10000")),
|
||||
dataSegment(offFee5, []byte("fee: 1000")),
|
||||
dataSegment(offFee6, []byte("fee: 100")),
|
||||
// pad entry to make count = 19 (u(19) above) — actually 18 strings above
|
||||
// Let me count: pfxName, pfxAddr, registered, nameTaken, notFound, owner,
|
||||
// transferred, unauth, released, name, noName, insuff, fee1..fee6 = 18
|
||||
// Fix: change u(19) → u(18) — corrected below in final assembly.
|
||||
))
|
||||
// Recount: 18 data segments total — rebuild without the u() wrapper mismatch.
|
||||
dataSection = section(0x0B, cat(
|
||||
u(18),
|
||||
dataSegment(offPfxName, []byte("name:")),
|
||||
dataSegment(offPfxAddr, []byte("addr:")),
|
||||
dataSegment(offRegisteredPfx, []byte("registered: ")),
|
||||
dataSegment(offNameTakenPfx, []byte("name taken: ")),
|
||||
dataSegment(offNotFoundPfx, []byte("not found: ")),
|
||||
dataSegment(offOwnerPfx, []byte("owner: ")),
|
||||
dataSegment(offTransferredPfx, []byte("transferred: ")),
|
||||
dataSegment(offUnauthPfx, []byte("unauthorized: ")),
|
||||
dataSegment(offReleasedPfx, []byte("released: ")),
|
||||
dataSegment(offNamePfx, []byte("name: ")),
|
||||
dataSegment(offNoNamePfx, []byte("no name: ")),
|
||||
dataSegment(offInsuffPfx, []byte("insufficient: ")),
|
||||
dataSegment(offFee1, []byte("fee: 10000000")),
|
||||
dataSegment(offFee2, []byte("fee: 1000000")),
|
||||
dataSegment(offFee3, []byte("fee: 100000")),
|
||||
dataSegment(offFee4, []byte("fee: 10000")),
|
||||
dataSegment(offFee5, []byte("fee: 1000")),
|
||||
dataSegment(offFee6, []byte("fee: 100")),
|
||||
))
|
||||
|
||||
codeSection := section(0x0A, cat(
|
||||
u(11),
|
||||
bytesEqualBody(),
|
||||
memcpyBody(),
|
||||
logPrefixNameBody(),
|
||||
calcFeeBody(),
|
||||
buildKeyBody(),
|
||||
registerBody(),
|
||||
resolveBody(),
|
||||
lookupBody(),
|
||||
transferBody(),
|
||||
releaseBody(),
|
||||
feeBody(),
|
||||
))
|
||||
|
||||
module := cat(
|
||||
[]byte{0x00, 0x61, 0x73, 0x6d},
|
||||
[]byte{0x01, 0x00, 0x00, 0x00},
|
||||
typeSection,
|
||||
importSection,
|
||||
functionSection,
|
||||
memorySection,
|
||||
exportSection,
|
||||
dataSection,
|
||||
codeSection,
|
||||
)
|
||||
|
||||
out := "contracts/username_registry/username_registry.wasm"
|
||||
if err := os.WriteFile(out, module, 0644); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "write:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("Written %s (%d bytes)\n", out, len(module))
|
||||
}
|
||||
BIN
contracts/username_registry/username_registry.wasm
Normal file
BIN
contracts/username_registry/username_registry.wasm
Normal file
Binary file not shown.
50
contracts/username_registry/username_registry_abi.json
Normal file
50
contracts/username_registry/username_registry_abi.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"contract": "username_registry",
|
||||
"version": "1.0.0",
|
||||
"description": "Maps human-readable usernames to wallet addresses. Shorter names cost more to register. Fees go to the contract treasury.",
|
||||
"methods": [
|
||||
{
|
||||
"name": "register",
|
||||
"description": "Register a username for the caller. Fee = 10^(7 - min(len,6)) µT (1-char=10M, 2=1M, 3=100K, 4=10K, 5=1K, 6+=100). Caller must have sufficient balance.",
|
||||
"args": [
|
||||
{"name": "name", "type": "string", "description": "Username to register (max 64 chars, lowercase)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "resolve",
|
||||
"description": "Look up the wallet address that owns a username. Logs 'owner: <address>' or 'not found: <name>'.",
|
||||
"args": [
|
||||
{"name": "name", "type": "string", "description": "Username to look up"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lookup",
|
||||
"description": "Reverse lookup: find the username registered to a given address. Logs 'name: <username>' or 'no name: <address>'.",
|
||||
"args": [
|
||||
{"name": "address", "type": "string", "description": "Wallet address (hex pubkey)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "transfer",
|
||||
"description": "Transfer ownership of a username to another address. Only the current owner may call this.",
|
||||
"args": [
|
||||
{"name": "name", "type": "string", "description": "Username to transfer"},
|
||||
{"name": "new_owner", "type": "string", "description": "New owner address (hex pubkey)"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "release",
|
||||
"description": "Release a username registration. Only the current owner may call this.",
|
||||
"args": [
|
||||
{"name": "name", "type": "string", "description": "Username to release"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "fee",
|
||||
"description": "Query the registration fee for a given name length. Logs 'fee: <amount>' in µT.",
|
||||
"args": [
|
||||
{"name": "name", "type": "string", "description": "Name whose fee you want to check"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user