Skip to content
Merged
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
86 changes: 74 additions & 12 deletions src/libs/l2ps/L2PSTransactionExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ import Datasource from "@/model/datasource"
import { GCRMain } from "@/model/entities/GCRv2/GCR_Main"
import { L2PSTransaction } from "@/model/entities/L2PSTransactions"
import type { Transaction, GCREdit, INativePayload } from "@kynesyslabs/demosdk/types"
import { denomination } from "@kynesyslabs/demosdk"
import L2PSProofManager from "./L2PSProofManager"
import HandleGCR from "@/libs/blockchain/gcr/handleGCR"
import { canonicalizeAmountToOs } from "@/forks/amountCanonical"
import { isForkActive } from "@/forks/forkGates"
import { getSharedState } from "@/utilities/sharedState"
import log from "@/utilities/logger"
import { getErrorMessage } from "@/utilities/errorMessage"

Expand Down Expand Up @@ -201,36 +205,94 @@ export default class L2PSTransactionExecutor {
let affectedAccountsCount = 0

if (nativePayload.nativeOperation === "send") {
const [to, amount] = nativePayload.args as [string, number]
const [to, rawAmount] = nativePayload.args as [
string,
number | string,
]
const sender = tx.content.from as string

// Validate amount (type check, integer, and positive)
if (typeof amount !== "number" || !Number.isFinite(amount) || !Number.isInteger(amount) || amount <= 0) {
return { success: false, message: "Invalid amount: must be a positive integer" }
// Match the L1 native path: canonicalise the wire amount to
// an OS bigint through the fork-aware helper, then emit GCR
// edits in the magnitude the rest of the pipeline expects
// (OS string post-fork, legacy DEM number pre-fork). Without
// this the executor moves ~10^9× too little post-fork, and
// post-fork OS string amounts are rejected outright by the
// number-only validation that used to live here.
const referenceHeight =
getSharedState.lastBlockNumber ?? 0
const forkActive = isForkActive(
"osDenomination",
referenceHeight,
)

// Canonicalise the wire amount and the fee in the same try
// block so any unexpected failure surfaces with a uniform
// "Invalid amount" error path. `L2PS_TX_FEE` is a constant
// `1` today and will not throw, but keeping both calls in
// the same guard prevents a silent regression if either
// input shape ever drifts.
let amountCanonical: bigint
let feeCanonical: bigint
try {
amountCanonical = canonicalizeAmountToOs(
rawAmount,
forkActive,
)
// L2PS_TX_FEE is declared in DEM units (1 DEM).
// Canonicalise the same way as the wire amount so the
// balance check and the burn edit agree on units.
feeCanonical = canonicalizeAmountToOs(
L2PS_TX_FEE,
forkActive,
)
} catch (e) {
return {
success: false,
message: `Invalid amount: ${(e as Error).message}`,
}
}
if (amountCanonical <= 0n) {
return {
success: false,
message: "Invalid amount: must be a positive integer",
}
}

// Check sender balance in L1 state (amount + fee)
// Check sender balance in L1 state (amount + fee). The L1
// balance is persisted as an OS magnitude string, so compare
// bigint-to-bigint regardless of fork status.
const senderAccount = await this.getOrCreateL1Account(sender)
const totalRequired = BigInt(amount) + BigInt(L2PS_TX_FEE)
const totalRequired = amountCanonical + feeCanonical
if (BigInt(senderAccount.balance) < totalRequired) {
return {
success: false,
message: `Insufficient L1 balance: has ${senderAccount.balance}, needs ${totalRequired} (${amount} + ${L2PS_TX_FEE} fee)`,
message: `Insufficient L1 balance: has ${senderAccount.balance}, needs ${totalRequired} (${amountCanonical} + ${feeCanonical} fee)`,
}
}

// Ensure receiver account exists
await this.getOrCreateL1Account(to)

// Generate GCR edits for L1 state change
// These will be applied at consensus time
// Emit GCR edits in the magnitude downstream consumers expect:
// OS string post-fork (matches the serializerGate wire shape
// the SDK ≥ v3.0.0 emits); pre-fork carries the legacy DEM
// magnitude as a string too — `GCREditBalance.amount` accepts
// `number | string`, and a string avoids the silent precision
// loss that `Number(bigint)` would introduce for any amount
// above `Number.MAX_SAFE_INTEGER`.
const editAmount: string = forkActive
? denomination.toOsString(amountCanonical)
: amountCanonical.toString()
const editFee: string = forkActive
? denomination.toOsString(feeCanonical)
: feeCanonical.toString()

// 1. Burn the fee (remove from sender, no add anywhere)
gcrEdits.push({
type: "balance",
operation: "remove",
account: sender,
amount: L2PS_TX_FEE,
amount: editFee,
txhash: tx.hash,
isRollback: false,
})
Expand All @@ -241,15 +303,15 @@ export default class L2PSTransactionExecutor {
type: "balance",
operation: "remove",
account: sender,
amount: amount,
amount: editAmount,
txhash: tx.hash,
isRollback: false,
},
{
type: "balance",
operation: "add",
account: to,
amount: amount,
amount: editAmount,
txhash: tx.hash,
isRollback: false,
},
Expand Down
Loading