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
3 changes: 3 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,9 @@ The RealUnit purchase and sale flows historically lived under `/v1/realunit/brok
| `PUT /v1/realunit/sell/:id/broadcast` | Submits the user-signed EIP-1559 transaction to the network | No — broadcast only, no `readContract` |
| `PUT /v1/realunit/transfer` | Persists a wallet-to-wallet (W2W) transfer intent and returns the EIP-7702 delegation data to sign. Limit-exempt (on-chain REALU→REALU self-custody movement). | No — prepares the gasless transfer |
| `PUT /v1/realunit/transfer/:id/confirm` | Relays the user-signed EIP-7702 delegation for the stored transfer request; DFX pays gas from the dedicated W2W gas wallet (`REALUNIT_W2W_GAS_WALLET_*`), never the Sell/OTC relayer | No `readContract` — relays the user-authorized ERC20 transfer |
| `PUT /v1/realunit/swap` | IBAN-free REALU → ZCHF swap quote — creates a `TransactionRequestType.SWAP` request (proceeds stay in the user wallet, no fiat Sell route/payout). Gated by RealUnit registration + KYC Level 30 (who may use the feature). **Limit-exempt by design**: KYC trading limits apply at the fiat boundary (buy/sell), but this is a crypto → crypto, self-custody, on-chain swap, so the non-fiat RealUnit carve-out in `TransactionHelper.getLimits` means `QuoteError.LIMIT_EXCEEDED` never fires for this pair. Anchors the ZCHF estimate against the live on-chain sell price | **Yes** — `RealUnitBlockchainService.getBrokerbotSellPrice` |
| `PUT /v1/realunit/swap/:id/unsigned-transaction` | Builds the REALU `transferAndCall` swap tx WITHOUT the deposit sweep (ZCHF lands in the user wallet) | No — builds calldata only |
| `PUT /v1/realunit/swap/:id/broadcast` | Submits the user-signed swap EIP-1559 transaction to the network | No — broadcast only, no `readContract` |

Operational consequences:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,32 @@ describe('EvmClient - broadcast boundary', () => {
});
});
});

describe('EvmClient - getTransactionCount block tag', () => {
// Call the real method body with a stubbed provider so we can assert the block-tag
// forwarding without constructing a live EvmClient (same isolation pattern as the
// broadcast-boundary suite above).
const proto = EvmClient.prototype as any;

it('defaults to the latest (mined) nonce when no block tag is given', async () => {
const getTransactionCount = jest.fn().mockResolvedValue(5);
const client = Object.create(EvmClient.prototype);
client.provider = { getTransactionCount };

const result = await proto.getTransactionCount.call(client, '0xabc');

expect(result).toBe(5);
expect(getTransactionCount).toHaveBeenCalledWith('0xabc', 'latest');
});

it('forwards the pending block tag to count still-pending mempool txs', async () => {
const getTransactionCount = jest.fn().mockResolvedValue(7);
const client = Object.create(EvmClient.prototype);
client.provider = { getTransactionCount };

const result = await proto.getTransactionCount.call(client, '0xabc', 'pending');

expect(result).toBe(7);
expect(getTransactionCount).toHaveBeenCalledWith('0xabc', 'pending');
});
});
13 changes: 11 additions & 2 deletions src/integration/blockchain/shared/evm/evm-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ export abstract class EvmClient extends BlockchainClient {
return evmTokenBalances[0]?.balance ?? 0;
}

async getTokenBalanceWei(asset: Asset, address?: string): Promise<EthersNumber> {
const owner = address ?? this.walletAddress;
const contract = this.getERC20ContractForDex(asset.chainId);
return contract.balanceOf(owner);
}

async getTokenBalances(assets: Asset[], address?: string): Promise<BlockchainTokenBalance[]> {
const owner = address ?? this.walletAddress;
const evmTokenBalances: BlockchainTokenBalance[] = [];
Expand Down Expand Up @@ -178,8 +184,11 @@ export abstract class EvmClient extends BlockchainClient {
return block.timestamp;
}

async getTransactionCount(address: string): Promise<number> {
return this.provider.getTransactionCount(address);
// Defaults to the `latest` (mined) nonce. Pass `'pending'` to also count still-pending txs in the
// mempool, which is required when a follow-up tx is built before a prior tx of the same sender is mined
// (otherwise both would reuse the same nonce and collide).
async getTransactionCount(address: string, blockTag: ethers.providers.BlockTag = 'latest'): Promise<number> {
return this.provider.getTransactionCount(address, blockTag);
}

protected async getTokenGasLimitForAsset(token: Asset): Promise<EthersNumber> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class PaymentRequestMapper {
return this.toLnurlpInvoice(paymentActivation);

case Blockchain.ETHEREUM:
case Blockchain.SEPOLIA:
case Blockchain.ARBITRUM:
case Blockchain.OPTIMISM:
case Blockchain.BASE:
Expand Down
261 changes: 138 additions & 123 deletions src/subdomains/core/payment-link/enums/index.ts
Original file line number Diff line number Diff line change
@@ -1,123 +1,138 @@
import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';

export enum PaymentLinkStatus {
ACTIVE = 'Active',
INACTIVE = 'Inactive',
UNASSIGNED = 'Unassigned',
}

export enum PaymentLinkPaymentStatus {
PENDING = 'Pending',
COMPLETED = 'Completed',
CANCELLED = 'Cancelled',
EXPIRED = 'Expired',
}

export enum PaymentQuoteStatus {
ACTUAL = 'Actual',
CANCELLED = 'Cancelled',
EXPIRED = 'Expired',

TX_RECEIVED = 'TxReceived',
TX_CHECKBOT = 'TxCheckbot',
TX_MEMPOOL = 'TxMempool',
TX_BLOCKCHAIN = 'TxBlockchain',
TX_COMPLETED = 'TxCompleted',
TX_FAILED = 'TxFailed',
}

export const PaymentQuoteTxStates = [
PaymentQuoteStatus.TX_RECEIVED,
PaymentQuoteStatus.TX_CHECKBOT,
PaymentQuoteStatus.TX_MEMPOOL,
PaymentQuoteStatus.TX_BLOCKCHAIN,
PaymentQuoteStatus.TX_COMPLETED,
];

export const PaymentQuoteFinalStates = [
PaymentQuoteStatus.CANCELLED,
PaymentQuoteStatus.EXPIRED,
PaymentQuoteStatus.TX_CHECKBOT,
PaymentQuoteStatus.TX_MEMPOOL,
PaymentQuoteStatus.TX_BLOCKCHAIN,
PaymentQuoteStatus.TX_COMPLETED,
PaymentQuoteStatus.TX_FAILED,
];

export enum PaymentActivationStatus {
OPEN = 'Open',
CLOSED = 'Closed',
}

export enum PaymentLinkPaymentMode {
SINGLE = 'Single',
MULTIPLE = 'Multiple',
}

export enum PaymentStandard {
OPEN_CRYPTO_PAY = 'OpenCryptoPay',
LIGHTNING_BOLT11 = 'LightningBolt11',
PAY_TO_ADDRESS = 'PayToAddress',
}

export enum C2BPaymentProvider {
BINANCE_PAY = Blockchain.BINANCE_PAY,
KUCOIN_PAY = Blockchain.KUCOIN_PAY,
}

export enum C2BPaymentStatus {
WAITING = 'WAITING',
PENDING = 'PENDING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
REFUNDED = 'REFUNDED',
}

export enum StickerType {
CLASSIC = 'Classic',
BITCOIN_FOCUS = 'BitcoinFocus',
}

export enum PaymentLinkMode {
SINGLE = 'Single',
MULTIPLE = 'Multiple',
PUBLIC = 'Public',
}

export enum StickerQrMode {
CUSTOMER = 'Customer',
POS = 'Pos',
}

export enum PaymentMerchantStatus {
CREATED = 'Created',
PENDING = 'Pending',
PROCESSED = 'Processed',
}

// Blockchains where the payer broadcasts the tx themselves and submits the resulting txId.
// The API marks the quote `TX_MEMPOOL` as soon as the txId is submitted, without waiting
// for on-chain confirmation. This is by design — accepting mempool transactions is the
// core feature of this payment flow (block-time waits are too slow for point-of-sale).
//
// Opt-in on the merchant side via `PaymentLinkConfig.minCompletionStatus = TX_MEMPOOL`
// (the default). Merchants who need stronger guarantees can set `TX_BLOCKCHAIN` to require
// on-chain confirmation before the payment auto-completes.
//
// Risk profile: accepting pre-confirmation enables tx-replacement attacks (the payer can
// broadcast a conflicting tx before a block confirms). The feature is scoped to physical
// point-of-sale: the fraudster has to be on the merchant's premises to walk off with
// goods, which makes the attack high-effort and locally traceable. For remote/high-value
// payments merchants should require `TX_BLOCKCHAIN`.
//
// We deliberately do not call the chain's node to verify the txId here, because none of
// the providers we use today expose mempool transactions consistently (own Monero/Zano
// daemons do; Tatum-backed Tron/Cardano do not), and a node-side check that returns
// "not found" for legitimately-broadcast-but-not-yet-propagated txs would break the
// feature. The only validation done is structural (txId format) — see `doTxIdPayment`.
export const UnverifiedTxIdBlockchains = [Blockchain.MONERO, Blockchain.ZANO, Blockchain.TRON, Blockchain.CARDANO];

// Blockchains where user broadcasts tx and sends txId, API verifies tx confirmation
export const VerifiedTxIdBlockchains = [Blockchain.SOLANA, Blockchain.INTERNET_COMPUTER];

export const TxIdBlockchains = [...UnverifiedTxIdBlockchains, ...VerifiedTxIdBlockchains];
import { Blockchain } from 'src/integration/blockchain/shared/enums/blockchain.enum';

export enum PaymentLinkStatus {
ACTIVE = 'Active',
INACTIVE = 'Inactive',
UNASSIGNED = 'Unassigned',
}

export enum PaymentLinkPaymentStatus {
PENDING = 'Pending',
COMPLETED = 'Completed',
CANCELLED = 'Cancelled',
EXPIRED = 'Expired',
}

export enum PaymentQuoteStatus {
ACTUAL = 'Actual',
CANCELLED = 'Cancelled',
EXPIRED = 'Expired',

TX_RECEIVED = 'TxReceived',
TX_CHECKBOT = 'TxCheckbot',
TX_MEMPOOL = 'TxMempool',
TX_BLOCKCHAIN = 'TxBlockchain',
TX_COMPLETED = 'TxCompleted',
TX_FAILED = 'TxFailed',
}

export const PaymentQuoteTxStates = [
PaymentQuoteStatus.TX_RECEIVED,
PaymentQuoteStatus.TX_CHECKBOT,
PaymentQuoteStatus.TX_MEMPOOL,
PaymentQuoteStatus.TX_BLOCKCHAIN,
PaymentQuoteStatus.TX_COMPLETED,
];

export const PaymentQuoteFinalStates = [
PaymentQuoteStatus.CANCELLED,
PaymentQuoteStatus.EXPIRED,
PaymentQuoteStatus.TX_CHECKBOT,
PaymentQuoteStatus.TX_MEMPOOL,
PaymentQuoteStatus.TX_BLOCKCHAIN,
PaymentQuoteStatus.TX_COMPLETED,
PaymentQuoteStatus.TX_FAILED,
];

export enum PaymentActivationStatus {
OPEN = 'Open',
CLOSED = 'Closed',
}

export enum PaymentLinkPaymentMode {
SINGLE = 'Single',
MULTIPLE = 'Multiple',
}

export enum PaymentStandard {
OPEN_CRYPTO_PAY = 'OpenCryptoPay',
LIGHTNING_BOLT11 = 'LightningBolt11',
PAY_TO_ADDRESS = 'PayToAddress',
}

export enum C2BPaymentProvider {
BINANCE_PAY = Blockchain.BINANCE_PAY,
KUCOIN_PAY = Blockchain.KUCOIN_PAY,
}

export enum C2BPaymentStatus {
WAITING = 'WAITING',
PENDING = 'PENDING',
COMPLETED = 'COMPLETED',
FAILED = 'FAILED',
REFUNDED = 'REFUNDED',
}

export enum StickerType {
CLASSIC = 'Classic',
BITCOIN_FOCUS = 'BitcoinFocus',
}

export enum PaymentLinkMode {
SINGLE = 'Single',
MULTIPLE = 'Multiple',
PUBLIC = 'Public',
}

export enum StickerQrMode {
CUSTOMER = 'Customer',
POS = 'Pos',
}

export enum PaymentMerchantStatus {
CREATED = 'Created',
PENDING = 'Pending',
PROCESSED = 'Processed',
}

// EVM blockchains the payment-link engine accepts for signed-hex payments (PaymentRequestMapper +
// PaymentQuoteService.executeHexPayment). Includes the Sepolia testnet so Open CryptoPay is testable on
// non-PRD (DEV/LOC); on PRD Sepolia is filtered out of PaymentLinkBlockchains (via TestBlockchains), so no
// PRD payment-link can offer it and these EVM cases stay unreachable there.
export const PaymentLinkEvmHexBlockchains = [
Blockchain.ETHEREUM,
Blockchain.SEPOLIA,
Blockchain.ARBITRUM,
Blockchain.OPTIMISM,
Blockchain.BASE,
Blockchain.GNOSIS,
Blockchain.POLYGON,
Blockchain.BINANCE_SMART_CHAIN,
];

// Blockchains where the payer broadcasts the tx themselves and submits the resulting txId.
// The API marks the quote `TX_MEMPOOL` as soon as the txId is submitted, without waiting
// for on-chain confirmation. This is by design — accepting mempool transactions is the
// core feature of this payment flow (block-time waits are too slow for point-of-sale).
//
// Opt-in on the merchant side via `PaymentLinkConfig.minCompletionStatus = TX_MEMPOOL`
// (the default). Merchants who need stronger guarantees can set `TX_BLOCKCHAIN` to require
// on-chain confirmation before the payment auto-completes.
//
// Risk profile: accepting pre-confirmation enables tx-replacement attacks (the payer can
// broadcast a conflicting tx before a block confirms). The feature is scoped to physical
// point-of-sale: the fraudster has to be on the merchant's premises to walk off with
// goods, which makes the attack high-effort and locally traceable. For remote/high-value
// payments merchants should require `TX_BLOCKCHAIN`.
//
// We deliberately do not call the chain's node to verify the txId here, because none of
// the providers we use today expose mempool transactions consistently (own Monero/Zano
// daemons do; Tatum-backed Tron/Cardano do not), and a node-side check that returns
// "not found" for legitimately-broadcast-but-not-yet-propagated txs would break the
// feature. The only validation done is structural (txId format) — see `doTxIdPayment`.
export const UnverifiedTxIdBlockchains = [Blockchain.MONERO, Blockchain.ZANO, Blockchain.TRON, Blockchain.CARDANO];

// Blockchains where user broadcasts tx and sends txId, API verifies tx confirmation
export const VerifiedTxIdBlockchains = [Blockchain.SOLANA, Blockchain.INTERNET_COMPUTER];

export const TxIdBlockchains = [...UnverifiedTxIdBlockchains, ...VerifiedTxIdBlockchains];
Loading
Loading