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
211 lines
7.0 KiB
Go
211 lines
7.0 KiB
Go
//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)))
|
|
}
|