Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions app/appclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,27 @@ func (c *AppClient) GetAppChannelDeployedAddr(cid string) (ctype.Addr, error) {
return addr, nil
}

// EnsureAppChannelDeployed deploys the registered virtual condition contract
// on-chain if it isn't already, and returns the deployed address. Useful for
// callers that need the contract on-chain but don't have a query to run
// (e.g. `PayResolver.resolvePaymentByConditions`, which calls
// `VirtContractResolver.resolve(virtAddr)` and reverts with
// "Nonexistent virtual address" otherwise).
func (c *AppClient) EnsureAppChannelDeployed(cid string) (ctype.Addr, error) {
appChannel := c.GetAppChannel(cid)
if appChannel == nil {
return ctype.ZeroAddr, fmt.Errorf("EnsureAppChannelDeployed: app channel not found")
}
return c.deployIfNeeded(appChannel)
}

// GetBooleanOutcome queries `IBooleanCond.{isFinalized,getOutcome}` for the
// registered condition contract, triggering deploy-on-query if the virtual
// contract has not been deployed yet. The query bytes are passed through
// unchanged (matches what `PayResolver` does on-chain) — no `SessionQuery`
// wrapping.
// contract has not been deployed yet. The same query bytes go to both calls
// (matches what `PayResolver` does on-chain). When `isFinalized` returns
// false `getOutcome` is not called — `PayResolver` short-circuits the same
// way, and a strict `IBooleanCond` is free to revert on `getOutcome` before
// finalization.
func (c *AppClient) GetBooleanOutcome(cid string, query []byte) (bool, bool, error) {
appChannel := c.GetAppChannel(cid)
if appChannel == nil {
Expand All @@ -156,6 +172,9 @@ func (c *AppClient) GetBooleanOutcome(cid string, query []byte) (bool, bool, err
if err != nil {
return false, false, fmt.Errorf("contract IsFinalized error: %w", err)
}
if !finalized {
return false, false, nil
}
result, err := contract.GetOutcome(&bind.CallOpts{}, query)
if err != nil {
return false, false, fmt.Errorf("contract GetOutcome error: %w", err)
Expand Down
12 changes: 3 additions & 9 deletions celersdk/appsession.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
// Copyright 2018-2025 Celer Network

// SDK APIs dealing with app sessions backed by stateless `IBooleanCond`
// virtual condition contracts.
//
// Post-trim the legacy gaming surface (turn-based state-exchange protocol with
// `SignAppData` / `HandleMatchData` / opcodes / seqnum tracking, on-chain
// `applyAction` / introspection / oracle disputes, the `NewAppSessionOnDeployedContract`
// path and its multisession dependencies) is gone. What remains is the thin
// wrapper around `client.CelerClient`'s registration / outcome-query surface
// for VIRTUAL_CONTRACT condition contracts.
// SDK APIs for app sessions backed by stateless `IBooleanCond` virtual
// condition contracts. Thin wrapper around `client.CelerClient`'s
// registration and outcome-query surface for VIRTUAL_CONTRACT conditions.

package celersdk

Expand Down
44 changes: 7 additions & 37 deletions celersdk/pay.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,33 +45,6 @@ func (mc *Client) SendToken(tk *Token, receiver string, amtWei string, noteTypeU
return ret, nil
}

func (mc *Client) SendETHWithCondition(receiver string, amtWei string, cond *BooleanCondition) (string, error) {
return mc.SendTokenWithCondition(nil, receiver, amtWei, cond)
}

// When should we call onSent? or do we need a new callback func?
func (mc *Client) SendTokenWithCondition(tk *Token, receiver string, amtWei string, cond *BooleanCondition) (string, error) {
xfer := createXfer(tk, receiver, amtWei)
timeout := cPayTimeout
if cond.TimeoutSec > 0 {
timeout = cond.TimeoutSec
}
condition, err := bc2c(cond)
if err != nil {
log.Errorln("SendTokenWithCondition:", err)
return ctype.ZeroPayIDHex, err
}
payID, err := mc.c.AddBooleanPay(
xfer, []*entity.Condition{condition}, uint64(time.Now().Unix())+uint64(timeout), nil /*note*/, 0)
if err != nil {
log.Errorln("SendTokenWithCondition:", err)
return ctype.ZeroPayIDHex, err
}
ret := ctype.PayID2Hex(payID)
log.Debugln("Sent pay:", ret)
return ret, nil
}

// ConfirmPay settles the condpay, ie. actually paid to pay dest
func (mc *Client) ConfirmPay(payID string) error {
return mc.c.ConfirmBooleanPay(ctype.Hex2PayID(payID))
Expand All @@ -90,14 +63,11 @@ func (mc *Client) RemoveExpiredPays(tk *Token) error {

// ResolvePayOnChain settles the payment onchain and receives the payment from OSP.
//
// VIRTUAL_CONTRACT prerequisite: PayResolver.resolvePaymentByConditions calls
// VirtContractResolver.resolve(virtAddr) on-chain, which reverts with
// "Nonexistent virtual address" if the virtual condition contract has not been
// deployed yet. The surviving deploy path is the deploy-on-query side effect
// of AppSession.OnChainGetBooleanOutcome — calling it once after registration
// (e.g. with a no-op query) ensures the virtual contract has bytecode by the
// time this resolve tx lands. DEPLOYED_CONTRACT conditions have no such
// prerequisite.
// VIRTUAL_CONTRACT conditions are deployed automatically before the resolve tx
// is submitted: this client walks the pay's conditions and, for any
// VIRTUAL_CONTRACT registered locally via NewAppChannelOnVirtualContract,
// triggers the same deploy path used by OnChainGetBooleanOutcome. Conditions
// registered on a different node are left to that node to deploy.
func (mc *Client) ResolvePayOnChain(payID string) error {
err := mc.c.ResolveCondPayOnChain(ctype.Hex2PayID(payID))
if err != nil {
Expand Down Expand Up @@ -144,8 +114,8 @@ func (mc *Client) GetOnChainPaymentInfo(paymentID string) (*OnChainPaymentInfo,
}

// ResolveIncomingPaymentOnChain submits PayResolver.resolvePaymentByConditions
// for the given payment. See ResolvePayOnChain doc for the VIRTUAL_CONTRACT
// deploy-before-resolve prerequisite — the same applies here.
// for the given payment. Locally-registered VIRTUAL_CONTRACT conditions are
// auto-deployed first (see ResolvePayOnChain doc).
func (mc *Client) ResolveIncomingPaymentOnChain(payId string) error {
return mc.c.ResolveCondPayOnChain(ctype.Hex2PayID(payId))
}
Expand Down
8 changes: 0 additions & 8 deletions celersdk/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,6 @@ type Balance struct {
ReceivingCap string
}

type BooleanCondition struct {
OnChainDeployed bool
OnChainAddress string // on-chain IBooleanCond contract address if OnChainDeployed is true
VirtualContractAddress string // deterministic virtual-contract address (hex) from CreateAppSessionOnVirtualContract; ignored if OnChainDeployed is true
ArgsForQueryOutcome []byte
TimeoutSec int // pay deadline = wall-clock unix time + TimeoutSec
}

type Token struct {
Erctype string // ERC20, ERC721 etc.
Addr string // token contract addr
Expand Down
15 changes: 0 additions & 15 deletions celersdk/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,21 +54,6 @@ func createXfer(tk *Token, receiver, amtWei string) *entity.TokenTransfer {
return xfer
}

func bc2c(bc *BooleanCondition) (*entity.Condition, error) {
if bc.OnChainDeployed {
return &entity.Condition{
ConditionType: entity.ConditionType_DEPLOYED_CONTRACT,
DeployedContractAddress: ctype.Hex2Addr(bc.OnChainAddress).Bytes(),
ArgsQueryOutcome: bc.ArgsForQueryOutcome,
}, nil
}
return &entity.Condition{
ConditionType: entity.ConditionType_VIRTUAL_CONTRACT,
VirtualContractAddress: ctype.Hex2Bytes(bc.VirtualContractAddress),
ArgsQueryOutcome: bc.ArgsForQueryOutcome,
}, nil
}

func sdkToken2entityToken(tk *Token) *entity.TokenInfo {
var token *entity.TokenInfo
if tk == nil { // ETH case
Expand Down
44 changes: 43 additions & 1 deletion client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package client

import (
"errors"
"fmt"
"math/big"
"time"

Expand Down Expand Up @@ -219,11 +220,52 @@ func (c *CelerClient) SignState(in []byte) []byte {
return c.cNode.SignState(in)
}

// ResolveCondPayOnChain tries to resolve a payment onchain in the PayRegistry
// ResolveCondPayOnChain tries to resolve a payment onchain in the PayRegistry.
// Before submitting the resolve tx, it walks the pay's `VIRTUAL_CONTRACT`
// conditions and deploys any that are still bytecode-only. PayResolver's
// `resolvePaymentByConditions` calls `VirtContractResolver.resolve(virtAddr)`
// and reverts with "Nonexistent virtual address" if the virtual contract
// hasn't been deployed yet — handling that here makes the resolve API
// self-sufficient instead of silently requiring callers to first invoke
// `OnChainGetBooleanOutcome` for its deploy-on-query side effect.
//
// We can only deploy contracts this node registered locally (the bytecode +
// constructor live in `AppClient.appChannels`); for conditions registered
// elsewhere we trust that the registering side already deployed and let the
// resolve tx fail loudly if not.
func (c *CelerClient) ResolveCondPayOnChain(payID ctype.PayIDType) error {
if err := c.ensureVirtualConditionsDeployed(payID); err != nil {
return err
}
return c.cNode.Disputer.SettleConditionalPay(payID)
}

func (c *CelerClient) ensureVirtualConditionsDeployed(payID ctype.PayIDType) error {
pay, _, found, err := c.cNode.GetDAL().GetPayment(payID)
if err != nil {
return err
}
if !found || pay == nil {
return common.ErrPayNotFound
}
for _, cond := range pay.GetConditions() {
if cond.GetConditionType() != entity.ConditionType_VIRTUAL_CONTRACT {
continue
}
cid := ctype.Bytes2Hex(cond.GetVirtualContractAddress())
// Locally registered? Trigger deploy-if-needed. Not registered locally
// is the cross-node case — leave it to the registering side; the
// resolve tx will surface a contract revert if neither side deploys.
if c.cNode.AppClient.GetAppChannel(cid) == nil {
continue
}
if _, err := c.cNode.AppClient.EnsureAppChannelDeployed(cid); err != nil {
return fmt.Errorf("ensure virt-contract %s deployed: %w", cid, err)
}
}
return nil
}

func (c *CelerClient) GetCondPayInfoFromRegistry(payID ctype.PayIDType) (*big.Int, uint64, error) {
return c.cNode.Disputer.GetCondPayInfoFromRegistry(payID)
}
Expand Down
1 change: 1 addition & 0 deletions cnode/cnode.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,7 @@ func (c *CNode) initialize(
c.nodeConfig,
c.masterTransactor,
c.monitorService,
c.ethclient,
c.dal,
c.signer,
c.bcastSend,
Expand Down
Loading
Loading