Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
0c22263
feat(tempo): add funding requirements and policies
jxom Sep 23, 2026
b466938
test(tempo): run funding conformance in ci
jxom Sep 23, 2026
86043af
test(tempo): remove funding e2e suite
jxom Sep 23, 2026
6cdf9bd
feat(tempo): add funding requirement constructor
jxom Sep 23, 2026
729d50c
test(tempo): move behavior cases last
jxom Sep 23, 2026
3c2408b
docs: format jsdoc examples
jxom Sep 23, 2026
99c0b75
feat(tempo): use to for funding requirement sources
jxom Sep 23, 2026
45c2c9d
refactor(tempo): rename dex funding source
jxom Sep 23, 2026
913cd5c
feat: consolidate tempo funding source API
jxom Sep 23, 2026
049a963
refactor: split tempo dex funding source
jxom Sep 23, 2026
6ca8bd2
docs: format dex funding source examples
jxom Sep 23, 2026
ae6f28a
refactor: name dex funding data encoder
jxom Sep 23, 2026
7cfe239
feat: expose native dex source on FundingSource
jxom Sep 23, 2026
972e0dd
fix(tempo): use to for funding policy sources
jxom Sep 24, 2026
d693160
feat(tempo): distinguish funding payload encoders
jxom Sep 24, 2026
57a4764
refactor(tempo): restore dex funding source module
jxom Sep 24, 2026
1c12f19
feat: add earn funding source codecs
jxom Sep 24, 2026
07c47b2
refactor: name funding source data decoders
jxom Sep 24, 2026
ff994c0
feat: add earn funding source constructor
jxom Sep 24, 2026
d03dd39
feat(tempo): add unsigned key authorization rpc conversion
jxom Sep 25, 2026
480b38b
fix(tempo): use to for funding rpc sources
jxom Sep 27, 2026
aa34265
fix: restore funding rpc target fields
jxom Sep 27, 2026
9e96afb
refactor(tempo): use target for funding sources
jxom Sep 27, 2026
a1001d3
style(tempo): format funding sources
jxom Sep 27, 2026
b639383
feat(tempo): support unsigned funding intent
jxom Sep 28, 2026
c3b6242
refactor(tempo): rename funding request types
jxom Sep 28, 2026
f5cb157
feat(tempo): support optional funding source ordering
jxom Sep 28, 2026
1e8e72f
test(tempo): remove funding policy e2e tests
jxom Sep 28, 2026
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
11 changes: 11 additions & 0 deletions .changeset/tempo-funding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"ox": patch

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mark the new public API as a minor release

When Changesets versions this commit, patch will publish these additions as a bug-fix release even though the commit adds new public subpath exports and new requireFunds/fundingPolicy API fields. This is an additive feature and should use a minor changeset so consumers can rely on patch releases not expanding the public API.

Useful? React with 👍 / 👎.

---

Added Tempo funding codecs, unsigned requirement inference, policy rules with optional source ordering, source helpers, and access key funding authorization.

```ts
import { TransactionRequest } from 'ox/tempo'

TransactionRequest.toRpc({ requireFunds: [{ sources: [] }] })
```
35 changes: 35 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -781,6 +781,31 @@
"types": "./dist/tempo/EarnShares.d.ts",
"default": "./dist/tempo/EarnShares.js"
},
"./tempo/FundingPolicy": {
"src": "./src/tempo/FundingPolicy.ts",
"types": "./dist/tempo/FundingPolicy.d.ts",
"default": "./dist/tempo/FundingPolicy.js"
},
"./tempo/FundingRequirement": {
"src": "./src/tempo/FundingRequirement.ts",
"types": "./dist/tempo/FundingRequirement.d.ts",
"default": "./dist/tempo/FundingRequirement.js"
},
"./tempo/FundingSource": {
"src": "./src/tempo/FundingSource.ts",
"types": "./dist/tempo/FundingSource.d.ts",
"default": "./dist/tempo/FundingSource.js"
},
"./tempo/FundingSourceDex": {
"src": "./src/tempo/FundingSourceDex.ts",
"types": "./dist/tempo/FundingSourceDex.d.ts",
"default": "./dist/tempo/FundingSourceDex.js"
},
"./tempo/FundingSourceEarn": {
"src": "./src/tempo/FundingSourceEarn.ts",
"types": "./dist/tempo/FundingSourceEarn.d.ts",
"default": "./dist/tempo/FundingSourceEarn.js"
},
"./tempo/KeyAuthorization": {
"src": "./src/tempo/KeyAuthorization.ts",
"types": "./dist/tempo/KeyAuthorization.d.ts",
Expand Down Expand Up @@ -1226,6 +1251,16 @@
"types": "./dist/zod/tempo/AuthorizationTempo.d.ts",
"default": "./dist/zod/tempo/AuthorizationTempo.js"
},
"./zod/tempo/FundingPolicy": {
"src": "./src/zod/tempo/FundingPolicy.ts",
"types": "./dist/zod/tempo/FundingPolicy.d.ts",
"default": "./dist/zod/tempo/FundingPolicy.js"
},
"./zod/tempo/FundingRequirement": {
"src": "./src/zod/tempo/FundingRequirement.ts",
"types": "./dist/zod/tempo/FundingRequirement.d.ts",
"default": "./dist/zod/tempo/FundingRequirement.js"
},
"./zod/tempo/KeyAuthorization": {
"src": "./src/zod/tempo/KeyAuthorization.ts",
"types": "./dist/zod/tempo/KeyAuthorization.d.ts",
Expand Down
5 changes: 4 additions & 1 deletion src/core/Frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,10 @@ export declare namespace fromRpc {
* ```ts twoslash
* import { Frame } from 'ox'
*
* const frame = Frame.from({ executionGas: 50_000n, mode: 'verify' })
* const frame = Frame.from({
* executionGas: 50_000n,
* mode: 'verify'
* })
* const rpc = Frame.toRpc(frame)
* ```
*
Expand Down
4 changes: 3 additions & 1 deletion src/core/TxEnvelope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,9 @@ export declare namespace deserialize {
*
* const envelope = TransactionEnvelope.from({
* chainId: 1,
* frames: [Frame.from({ executionGas: 50_000n, mode: 'sender' })],
* frames: [
* Frame.from({ executionGas: 50_000n, mode: 'sender' })
* ],
* sender: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8'
* })
* ```
Expand Down
204 changes: 204 additions & 0 deletions src/tempo/FundingPolicy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import { describe, expect, test } from 'vp/test'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Move the new unit tests under _test

Relocate this test, along with FundingRequirement.test.ts, FundingSource.test.ts, and FundingSourceDex.test.ts, into colocated _test directories. Adding all four directly under src/tempo/ extends the legacy layout instead of following the repository’s required src/**/_test/*.test.ts structure.

AGENTS.md reference: AGENTS.md:L92-L92

Useful? React with 👍 / 👎.

import * as Rlp from '../core/Rlp.js'
import * as KeyAuthorization from './KeyAuthorization.js'
import * as FundingPolicy from './FundingPolicy.js'

const token = '0x0101010101010101010101010101010101010101' as const
const target = '0x0202020202020202020202020202020202020202' as const
const requirement = {
token,
amount: 50n,
sources: [{ target, data: '0xab' as const }],
slippageBps: 100,
}

const rules = {
enforceOrder: false,
maxSlippageBps: 100,
sources: { [token]: requirement.sources },
}
describe('encode', () => {
test('ABI encodes, decodes and hashes canonical rules', () => {
expect(FundingPolicy.decode(FundingPolicy.encode(rules))).toEqual(rules)
expect(FundingPolicy.hash(rules)).toMatch(/^0x[0-9a-f]{64}$/)
expect(() =>
FundingPolicy.decode(`${FundingPolicy.encode(rules)}00`),
).toThrow()
})
})

describe('hash', () => {
test('canonical token order preserves significant source order', () => {
const second = '0x0303030303030303030303030303030303030303'
const first = {
...rules,
sources: { [second]: requirement.sources, [token]: requirement.sources },
}
const reordered = {
...rules,
sources: { [token]: requirement.sources, [second]: requirement.sources },
}
expect(FundingPolicy.encode(first)).toBe(FundingPolicy.encode(reordered))
const sources = [...requirement.sources, { target, data: '0xcd' as const }]
expect(
FundingPolicy.hash({ ...rules, sources: { [token]: sources } }),
).not.toBe(
FundingPolicy.hash({
...rules,
sources: { [token]: [...sources].reverse() },
}),
)
})
})

describe('toTuple', () => {
test('rejects invalid policy IDs and admins', () => {
for (const value of [
0n,
2n ** 64n,
{ admins: [], rules },
{ admins: [token, token], rules },
])
expect(() => FundingPolicy.toTuple(value)).toThrow()
})
})

describe('toRoutes', () => {
test('maps source addresses to the contract ABI field', () => {
expect(FundingPolicy.toRoutes(rules)).toEqual([
{ token, sources: [{ target, data: '0xab' }] },
])
})
})

describe('toRpc', () => {
test('keeps the RPC target field', () => {
expect(FundingPolicy.toRpc({ admins: [token], rules })).toEqual({
admins: [token],
rules: {
enforceOrder: false,
maxSlippageBps: 100,
sources: { [token]: [{ target, data: '0xab' }] },
},
})
})
})

describe('fromRpc', () => {
test('preserves RPC source targets', () => {
expect(
FundingPolicy.fromRpc({
admins: [token],
rules: {
enforceOrder: false,
maxSlippageBps: 100,
sources: { [token]: [{ target, data: '0xab' }] },
},
}),
).toEqual({ admins: [token], rules })
})
})

describe('fromTuple', () => {
test('decodes source targets', () => {
expect(
FundingPolicy.fromTuple([
[token],
['0x64', [[token, [[target, '0xab']]]], '0x'],
]),
).toEqual({ admins: [token], rules })
})
})

describe('behavior', () => {
test.each([undefined, false, true])(
'round-trips ordering (%s)',
(enforceOrder) => {
const policy = { admins: [token], rules: { ...rules, enforceOrder } }
const expected = {
...policy,
rules: { ...policy.rules, enforceOrder: enforceOrder ?? false },
}
expect(FundingPolicy.decode(FundingPolicy.encode(policy.rules))).toEqual(
expected.rules,
)
expect(FundingPolicy.fromTuple(FundingPolicy.toTuple(policy))).toEqual(
expected,
)
expect(FundingPolicy.fromRpc(FundingPolicy.toRpc(policy))).toEqual(policy)
const authorization = {
address: target,
chainId: 1n,
fundingPolicy: policy,
type: 'secp256k1' as const,
}
expect(
KeyAuthorization.deserialize(KeyAuthorization.serialize(authorization)),
).toEqual({
...authorization,
fundingPolicy: expected,
})
},
)

test('omitted ordering encodes identically to false', () => {
const { enforceOrder: _, ...unordered } = rules
expect(FundingPolicy.encode(unordered)).toBe(FundingPolicy.encode(rules))
expect(FundingPolicy.hash(unordered)).toBe(FundingPolicy.hash(rules))
expect(
FundingPolicy.toTuple({ admins: [token], rules: unordered }),
).toEqual(FundingPolicy.toTuple({ admins: [token], rules }))
})

test('ordering changes the commitment and authorization signature', () => {
const ordered = { ...rules, enforceOrder: true }
expect(FundingPolicy.hash(ordered)).not.toBe(FundingPolicy.hash(rules))
const authorization = {
address: target,
chainId: 1n,
fundingPolicy: { admins: [token], rules },
type: 'secp256k1' as const,
}
expect(KeyAuthorization.getSignPayload(authorization)).not.toBe(
KeyAuthorization.getSignPayload({
...authorization,
fundingPolicy: { admins: [token], rules: ordered },
}),
)
})

test('uses the canonical rules wire layout', () => {
for (const enforceOrder of [false, true]) {
const empty = { enforceOrder, maxSlippageBps: 0, sources: {} }
const abi = `0x${[
'20', // Dynamic tuple offset.
'00', // Slippage.
'60', // Routes offset within the tuple.
enforceOrder ? '01' : '00',
'00', // Empty routes.
]
.map((word) => word.padStart(64, '0'))
.join('')}`
expect(FundingPolicy.encode(empty)).toBe(abi)
const tuple = FundingPolicy.toTuple({ admins: [token], rules: empty })
if (typeof tuple === 'string')
throw new Error('Expected an inline policy.')
expect(Rlp.fromHex(tuple[1])).toBe(
enforceOrder ? '0xc380c001' : '0xc380c080',
)
}
})

test('rejects invalid and missing ordering flags', () => {
for (const enforceOrder of [0, 1, 'false', null])
expect(() =>
FundingPolicy.encode({ ...rules, enforceOrder } as never),
).toThrowErrorMatchingInlineSnapshot(
`[FundingPolicy.InvalidPolicyError: Ordering enforcement must be a boolean.]`,
)
for (const suffix of [[], ['0x00'], ['0x02'], ['0x0001']])
expect(() =>
FundingPolicy.fromTuple([[token], ['0x', [], ...suffix]] as never),
).toThrow()
})
})
Loading
Loading