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
819 lines
30 KiB
Go
819 lines
30 KiB
Go
// 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))
|
|
}
|