Skip to content
6 changes: 6 additions & 0 deletions .changeset/dev-2-bridge-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@polymarket/bindings': minor
'@polymarket/client': minor
---

Add typed account-funding workflows for deposit and withdrawal addresses, supported assets, quotes, and paginated transfer status.
7 changes: 7 additions & 0 deletions packages/bindings/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./bridge": {
"types": "./dist/bridge/index.d.ts",
"default": "./dist/bridge/index.js"
},
"./clob": {
"types": "./dist/clob/index.d.ts",
"default": "./dist/clob/index.js"
Expand Down Expand Up @@ -49,6 +53,9 @@
},
"typesVersions": {
"*": {
"bridge": [
"./dist/bridge/index.d.ts"
],
"clob": [
"./dist/clob/index.d.ts"
],
Expand Down
105 changes: 105 additions & 0 deletions packages/bindings/src/bridge/funding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { describe, expect, it } from 'vitest';
import {
FundingAddressSetResponseSchema,
FundingTransactionsPageSchema,
KnownFundingTransactionStatus,
} from './funding';

describe('FundingAddressSetResponseSchema', () => {
it('normalizes the response and retains Tron addresses and warnings', () => {
const result = FundingAddressSetResponseSchema.parse({
address: {
evm: '0x0000000000000000000000000000000000000001',
svm: '11111111111111111111111111111111',
btc: 'bc1qexample',
tron: 'TExample',
},
note: 'Use the address for the source chain.',
warnings: [
{
code: 'missing_builder_code',
message: 'Include a builder code for attribution.',
},
],
});

expect(result).toEqual({
addresses: {
evm: '0x0000000000000000000000000000000000000001',
svm: '11111111111111111111111111111111',
btc: 'bc1qexample',
tron: 'TExample',
},
note: 'Use the address for the source chain.',
warnings: [
{
code: 'missing_builder_code',
message: 'Include a builder code for attribution.',
},
],
});
});

it('normalizes the documented TVM key to tron', () => {
const result = FundingAddressSetResponseSchema.parse({
address: {
evm: '0x0000000000000000000000000000000000000001',
svm: '11111111111111111111111111111111',
btc: 'bc1qexample',
tvm: 'TExample',
},
});

expect(result.addresses.tron).toBe('TExample');
expect(result.addresses).not.toHaveProperty('tvm');
});
});

describe('FundingTransactionsPageSchema', () => {
it('accepts detected transactions before optional metadata is available', () => {
const result = FundingTransactionsPageSchema.parse({
transactions: [
{
fromChainId: '1',
fromTokenAddress: '0x0000000000000000000000000000000000000002',
fromAmountBaseUnit: '1000000',
toChainId: '137',
toTokenAddress: '0x0000000000000000000000000000000000000003',
status: 'DEPOSIT_DETECTED',
},
],
nextCursor: 'eyJsYXN0SWQiOiI0MiJ9',
});

expect(result.transactions[0]?.status).toBe(
KnownFundingTransactionStatus.DepositDetected,
);
expect(result.transactions[0]?.createdTimeMs).toBeUndefined();
expect(result.transactions[0]?.txHash).toBeUndefined();
expect(result.nextCursor).toBe('eyJsYXN0SWQiOiI0MiJ9');
});

it('preserves newly introduced statuses', () => {
const result = FundingTransactionsPageSchema.parse({
transactions: [
{
fromChainId: '1',
fromTokenAddress: '0x0000000000000000000000000000000000000002',
fromAmountBaseUnit: '1000000',
toChainId: '137',
toTokenAddress: '0x0000000000000000000000000000000000000003',
status: 'REFUNDING',
},
],
nextCursor: null,
});

expect(result.transactions[0]?.status).toBe('REFUNDING');
});

it('treats a pre-pagination response as a terminal page', () => {
const result = FundingTransactionsPageSchema.parse({ transactions: [] });

expect(result.nextCursor).toBeNull();
});
});
145 changes: 145 additions & 0 deletions packages/bindings/src/bridge/funding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { z } from 'zod';
import {
BaseUnitsSchema,
EpochMillisecondsSchema,
EvmAddressSchema,
} from '../shared';

export enum KnownFundingTransactionStatus {
DepositDetected = 'DEPOSIT_DETECTED',
Processing = 'PROCESSING',
OriginTransactionConfirmed = 'ORIGIN_TX_CONFIRMED',
Submitted = 'SUBMITTED',
Completed = 'COMPLETED',
Failed = 'FAILED',
}

/**
* A funding transaction status. Known statuses are enumerated in
* {@link KnownFundingTransactionStatus}; newly introduced statuses flow
* through as plain strings.
*/
export type FundingTransactionStatus =
| KnownFundingTransactionStatus
| (string & {});

export const FundingTransactionStatusSchema = z
.string()
.transform((value): FundingTransactionStatus => value);

const FundingEvmAddressSchema = z
.string()
.regex(/^0x[0-9a-fA-F]{40}$/)
.pipe(EvmAddressSchema);

export const FundingAddressesSchema = z
.object({
evm: FundingEvmAddressSchema,
svm: z.string().min(1),
btc: z.string().min(1),
tron: z.string().min(1).optional(),
// Some documentation calls the Tron address type `tvm`.
tvm: z.string().min(1).optional(),
})
.transform(({ tvm, ...addresses }) => ({
...addresses,
...(addresses.tron === undefined && tvm !== undefined ? { tron: tvm } : {}),
}));

export type FundingAddresses = z.infer<typeof FundingAddressesSchema>;

export const FundingWarningSchema = z.object({
code: z.string(),
message: z.string(),
});

export type FundingWarning = z.infer<typeof FundingWarningSchema>;

export const FundingAddressSetResponseSchema = z
.object({
address: FundingAddressesSchema,
note: z.string().optional(),
warnings: z.array(FundingWarningSchema).optional(),
})
.transform((response) => ({
addresses: response.address,
...(response.note === undefined ? {} : { note: response.note }),
...(response.warnings === undefined ? {} : { warnings: response.warnings }),
}));

export type FundingAddressSet = z.infer<typeof FundingAddressSetResponseSchema>;

export const FundingTokenSchema = z.object({
name: z.string(),
symbol: z.string(),
address: z.string(),
decimals: z.number().int().nonnegative(),
});

export type FundingToken = z.infer<typeof FundingTokenSchema>;

export const FundingAssetSchema = z.object({
chainId: z.string(),
chainName: z.string(),
token: FundingTokenSchema,
minCheckoutUsd: z.number(),
});

export type FundingAsset = z.infer<typeof FundingAssetSchema>;

export const SupportedFundingAssetsResponseSchema = z.object({
supportedAssets: z.array(FundingAssetSchema),
note: z.string().optional(),
});

export const FundingFeeBreakdownSchema = z.object({
appFeeLabel: z.string(),
appFeePercent: z.number(),
appFeeUsd: z.number(),
fillCostPercent: z.number(),
fillCostUsd: z.number(),
gasUsd: z.number(),
maxSlippage: z.number(),
minReceived: z.number(),
swapImpact: z.number(),
swapImpactUsd: z.number(),
totalImpact: z.number(),
totalImpactUsd: z.number(),
});

export type FundingFeeBreakdown = z.infer<typeof FundingFeeBreakdownSchema>;

export const FundingQuoteSchema = z.object({
estCheckoutTimeMs: z.number().int().nonnegative(),
estFeeBreakdown: FundingFeeBreakdownSchema,
estInputUsd: z.number(),
estOutputUsd: z.number(),
estToTokenBaseUnit: BaseUnitsSchema,
quoteId: z.string().min(1),
});

export type FundingQuote = z.infer<typeof FundingQuoteSchema>;

export const FundingTransactionSchema = z.object({
fromChainId: z.string(),
fromTokenAddress: z.string(),
fromAmountBaseUnit: BaseUnitsSchema,
toChainId: z.string(),
toTokenAddress: z.string(),
status: FundingTransactionStatusSchema,
txHash: z.string().min(1).optional(),
createdTimeMs: EpochMillisecondsSchema.optional(),
});

export type FundingTransaction = z.infer<typeof FundingTransactionSchema>;

export const FundingTransactionsPageSchema = z.object({
transactions: z.array(FundingTransactionSchema),
// Production temporarily omits the new field during the pagination rollout.
nextCursor: z
.string()
.min(1)
.nullable()
.optional()
.transform((value) => value ?? null),
});
1 change: 1 addition & 0 deletions packages/bindings/src/bridge/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './funding';
1 change: 1 addition & 0 deletions packages/bindings/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { defineConfig } from 'tsup';
export default defineConfig(() => ({
entry: [
'src/index.ts',
'src/bridge/index.ts',
'src/clob/index.ts',
'src/combos/index.ts',
'src/data/index.ts',
Expand Down
33 changes: 33 additions & 0 deletions packages/client/src/actions/funding.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { describe, it } from 'vitest';
import type { PublicFundingActions, SecureFundingActions } from '../decorators';

describe('account-bound funding action types', () => {
it('requires public callers to choose a wallet', () => {
const actions = {} as PublicFundingActions;

actions.createDepositAddresses({
wallet: '0x0000000000000000000000000000000000000001',
});
// @ts-expect-error Public clients do not have an account-bound wallet.
actions.createDepositAddresses();
});

it('does not allow secure callers to override the account wallet', () => {
const actions = {} as SecureFundingActions;

actions.createDepositAddresses();
actions.createDepositAddresses({
// @ts-expect-error Secure funding actions always use client.account.wallet.
wallet: '0x0000000000000000000000000000000000000001',
});
actions.createWithdrawalAddresses({
destination: {
chainId: '137',
recipientAddress: '0x0000000000000000000000000000000000000001',
tokenAddress: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359',
},
// @ts-expect-error Secure funding actions always use client.account.wallet.
wallet: '0x0000000000000000000000000000000000000001',
});
});
});
Loading