tolk 1.3 // Example: price-aware vault — a minimal Phoebe consumer. // // Pattern: deposit → ask Phoebe for a verified price → quote a withdrawal // against that price → pay out. Replace the payout math with whatever // pricing logic your dapp needs (settlement, liquidation, option strike, // DEX fill, etc.). // // This is a reference implementation for `@titon-network/phoebe-sdk` — // not audited, NOT production-ready. Use as a starting template. const E_NOT_OWNER: int = 100; const E_NOT_PHOEBE: int = 201; const E_NO_PENDING: int = 110; const E_INSUFFICIENT_BAL: int = 111; const E_AMOUNT_ZERO: int = 112; // ===== Cross-contract wire types (BYTE-IDENTICAL to phoebe/messages.tolk) ===== struct PriceLeaf { feedId: uint16 mantissa: int128 expo: int8 confBps: uint16 pubTime: uint32 source: uint8 } struct (0x00000072) FulfillPrice { queryId: uint64 leaf: PriceLeaf } struct (0x00000071) RequestPrice { queryId: uint64 feedId: uint16 proofRef: cell leaf: PriceLeaf maxStaleness: uint32 } // ===== Vault-specific ops ===== /// Anyone deposits TON into the vault. struct (0x00000010) Deposit {} /// A depositor asks for a price-quoted withdrawal. Carries the leaf + proof /// the vault will forward to Phoebe; the FulfillPrice callback closes the /// loop and triggers the actual payout. struct (0x00000011) RequestWithdraw { amount: coins feedId: uint16 proofRef: cell leaf: PriceLeaf maxStaleness: uint32 } type VaultMessage = | Deposit | RequestWithdraw | FulfillPrice; // ===== Storage ===== // // `pending` keys by queryId so multiple concurrent withdrawals can be in // flight. Each entry records the requesting depositor + withdrawal amount // so the FulfillPrice callback can pay out without trusting user input. struct PendingWithdraw { depositor: address amount: coins } struct VaultStorage { owner: address phoebe: address nextQueryId: uint64 pending: map } fun VaultStorage.load(): VaultStorage { return VaultStorage.fromCell(contract.getData()); } fun VaultStorage.save(self) { contract.setData(self.toCell()); } // ===== Receivers ===== fun handleDeposit(_msg: Deposit, _sender: address) { // Deposit is value-only — TON sticks in the contract balance. No state // mutation needed; the vault's "shares" model is the contract balance // itself in this minimal example. } fun handleRequestWithdraw(msg: RequestWithdraw, sender: address) { assert (msg.amount > 0) throw E_AMOUNT_ZERO; var storage = lazy VaultStorage.load(); val queryId = storage.nextQueryId; storage.nextQueryId = queryId + 1; storage.pending.set(queryId, PendingWithdraw { depositor: sender, amount: msg.amount, }); storage.save(); val req = createMessage({ bounce: BounceMode.NoBounce, value: ton("0.1"), // gas + pull fee — sized generously for the example dest: storage.phoebe, body: RequestPrice { queryId: queryId, feedId: msg.feedId, proofRef: msg.proofRef, leaf: msg.leaf, maxStaleness: msg.maxStaleness, }, }); req.send(SEND_MODE_REGULAR); } fun handleFulfillPrice(msg: FulfillPrice, sender: address) { var storage = lazy VaultStorage.load(); assert (sender == storage.phoebe) throw E_NOT_PHOEBE; val pendingFound = storage.pending.get(msg.queryId); assert (pendingFound.isFound) throw E_NO_PENDING; val pending = pendingFound.loadValue(); storage.pending.delete(msg.queryId); storage.save(); // Quote payout = amount × (mantissa × 10^expo). For the example we just // pass the requested amount through; a real vault would multiply by // the price to convert between asset units (TON ↔ USD, etc.). // // CUSTOMIZE: replace the line below with your pricing math. val payout = pending.amount; // CUSTOMIZE: assert the vault can pay out (e.g. balance check, share // accounting). The minimal example uses contract balance directly. assert (contract.getBalance() >= payout) throw E_INSUFFICIENT_BAL; val transfer = createMessage({ bounce: BounceMode.NoBounce, value: payout, dest: pending.depositor, }); transfer.send(SEND_MODE_PAY_FEES_SEPARATELY); } fun onInternalMessage(in: InMessage) { val sender: address = in.senderAddress; val msg = lazy VaultMessage.fromSlice(in.body); match (msg) { Deposit => handleDeposit(msg, sender), RequestWithdraw => handleRequestWithdraw(msg, sender), FulfillPrice => handleFulfillPrice(msg, sender), else => assert (in.body.isEmpty()) throw 0xFFFF, } } // ===== Getters ===== get fun owner(): address { return VaultStorage.load().owner; } get fun phoebe(): address { return VaultStorage.load().phoebe; } get fun nextQueryId(): uint64 { return VaultStorage.load().nextQueryId; } /// Returns (depositor, amount) for a pending withdrawal by queryId, or /// (addr_none, 0) if no such pending exists. get fun pending(queryId: uint64): (address, coins) { val s = VaultStorage.load(); val r = s.pending.get(queryId); if (!r.isFound) { return (createAddressNone(), 0); } val p = r.loadValue(); return (p.depositor, p.amount); }