Skip to content
Open
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
35 changes: 35 additions & 0 deletions packages/composable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,41 @@ class CustomOrder extends ConditionalOrder<DataType, StaticType> {
}
```

### JIT Poller

Build the calls for a just-in-time funded TWAP. The application owns the deployed poller address, which must use this ABI.

```typescript
import {
ComposableCowPollerSchedule,
encodePollFunds,
encodeRegister,
encodeRevoke,
getScheduleId,
} from '@cowprotocol/sdk-composable'
import { setGlobalAdapter } from '@cowprotocol/sdk-common'

setGlobalAdapter(adapter)

const schedule: ComposableCowPollerSchedule = {
handler: twapHandler,
funder: eoa,
owner: cowShed,
salt,
staticInput,
}

const id = getScheduleId(schedule)
// register and revoke may be called by the funder or its canonical CowShed
const registerCalldata = encodeRegister(schedule)
const preHook = {
target: pollerAddress,
callData: encodePollFunds(id),
gasLimit: '350000',
}
const revokeCalldata = encodeRevoke(id)
```

## Usage

```typescript
Expand Down
43 changes: 43 additions & 0 deletions packages/composable/src/ComposableCowPoller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { getGlobalAdapter } from '@cowprotocol/sdk-common'

import { ComposableCowPollerAbi } from './abis/ComposableCowPollerAbi'
import { ComposableCowPollerSchedule, ComposableCowPollerScheduleKey } from './types'

export { ComposableCowPollerAbi } from './abis/ComposableCowPollerAbi'

const SCHEDULE_ID_ABI = ['address', 'address', 'address', 'bytes32']

/**
* Calculates the app-data-independent poller schedule ID.
*/
export function getScheduleId(schedule: ComposableCowPollerScheduleKey): string {
const encoded = getGlobalAdapter().utils.encodeAbi(SCHEDULE_ID_ABI, [
schedule.funder,
schedule.handler,
schedule.owner,
schedule.salt,
])

return getGlobalAdapter().utils.keccak256(encoded)
}

/**
* Encodes a transaction that registers a JIT funding schedule.
*/
export function encodeRegister(schedule: ComposableCowPollerSchedule): string {
return getGlobalAdapter().utils.encodeFunction(ComposableCowPollerAbi, 'register', [schedule]) as string
}

/**
* Encodes the app-data pre-hook that pulls funds for one active TWAP part.
*/
export function encodePollFunds(id: string): string {
return getGlobalAdapter().utils.encodeFunction(ComposableCowPollerAbi, 'pollFunds', [id]) as string
}

/**
* Encodes a transaction that disables a JIT funding schedule.
*/
export function encodeRevoke(id: string): string {
return getGlobalAdapter().utils.encodeFunction(ComposableCowPollerAbi, 'revoke', [id]) as string
}
134 changes: 134 additions & 0 deletions packages/composable/src/abis/ComposableCowPollerAbi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
export const ComposableCowPollerAbi = [
{
type: 'constructor',
inputs: [
{ name: 'composableCow', type: 'address', internalType: 'contract ComposableCoW' },
{ name: 'cowShedFactory', type: 'address', internalType: 'contract ICowShedFactory' },
],
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'COMPOSABLE_COW',
inputs: [],
outputs: [{ name: '', type: 'address', internalType: 'contract ComposableCoW' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'COW_SHED_FACTORY',
inputs: [],
outputs: [{ name: '', type: 'address', internalType: 'contract ICowShedFactory' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'funded',
inputs: [
{ name: '', type: 'bytes32', internalType: 'bytes32' },
{ name: '', type: 'bytes32', internalType: 'bytes32' },
],
outputs: [{ name: '', type: 'bool', internalType: 'bool' }],
stateMutability: 'view',
},
{
type: 'function',
name: 'pollFunds',
inputs: [{ name: 'id', type: 'bytes32', internalType: 'bytes32' }],
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'register',
inputs: [
{
name: 'schedule',
type: 'tuple',
internalType: 'struct ComposableCowPoller.Schedule',
components: [
{ name: 'handler', type: 'address', internalType: 'contract IConditionalOrderGenerator' },
{ name: 'funder', type: 'address', internalType: 'address' },
{ name: 'owner', type: 'address', internalType: 'address' },
{ name: 'salt', type: 'bytes32', internalType: 'bytes32' },
{ name: 'staticInput', type: 'bytes', internalType: 'bytes' },
],
},
],
outputs: [{ name: 'id', type: 'bytes32', internalType: 'bytes32' }],
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'revoke',
inputs: [{ name: 'id', type: 'bytes32', internalType: 'bytes32' }],
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
name: 'scheduleId',
inputs: [
{
name: 'schedule',
type: 'tuple',
internalType: 'struct ComposableCowPoller.Schedule',
components: [
{ name: 'handler', type: 'address', internalType: 'contract IConditionalOrderGenerator' },
{ name: 'funder', type: 'address', internalType: 'address' },
{ name: 'owner', type: 'address', internalType: 'address' },
{ name: 'salt', type: 'bytes32', internalType: 'bytes32' },
{ name: 'staticInput', type: 'bytes', internalType: 'bytes' },
],
},
],
outputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }],
stateMutability: 'pure',
},
{
type: 'function',
name: 'schedules',
inputs: [{ name: '', type: 'bytes32', internalType: 'bytes32' }],
outputs: [
{ name: 'handler', type: 'address', internalType: 'contract IConditionalOrderGenerator' },
{ name: 'funder', type: 'address', internalType: 'address' },
{ name: 'owner', type: 'address', internalType: 'address' },
{ name: 'salt', type: 'bytes32', internalType: 'bytes32' },
{ name: 'staticInput', type: 'bytes', internalType: 'bytes' },
],
stateMutability: 'view',
},
{
type: 'event',
name: 'Pulled',
inputs: [
{ name: 'id', type: 'bytes32', indexed: true, internalType: 'bytes32' },
{ name: 'orderDigest', type: 'bytes32', indexed: false, internalType: 'bytes32' },
{ name: 'amount', type: 'uint256', indexed: false, internalType: 'uint256' },
],
anonymous: false,
},
{
type: 'event',
name: 'ScheduleRegistered',
inputs: [
{ name: 'id', type: 'bytes32', indexed: true, internalType: 'bytes32' },
{ name: 'owner', type: 'address', indexed: true, internalType: 'address' },
{ name: 'funder', type: 'address', indexed: true, internalType: 'address' },
],
anonymous: false,
},
{
type: 'event',
name: 'ScheduleRevoked',
inputs: [
{ name: 'id', type: 'bytes32', indexed: true, internalType: 'bytes32' },
{ name: 'owner', type: 'address', indexed: true, internalType: 'address' },
{ name: 'funder', type: 'address', indexed: true, internalType: 'address' },
],
anonymous: false,
},
{ type: 'error', name: 'NoSchedule', inputs: [] },
{ type: 'error', name: 'OrderNotLive', inputs: [] },
{ type: 'error', name: 'UnauthorizedCaller', inputs: [] },
] as const
1 change: 1 addition & 0 deletions packages/composable/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export * from './utils'
export * from './ConditionalOrder'
export * from './Multiplexer'
export * from './ConditionalOrderFactory'
export * from './ComposableCowPoller'
export * from './orderTypes'
13 changes: 13 additions & 0 deletions packages/composable/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ export type ConditionalOrderParams = {
readonly staticInput: string
}

/** Fields that determine a JIT poller schedule ID. */
export type ComposableCowPollerScheduleKey = {
readonly handler: string
readonly funder: string
readonly owner: string
readonly salt: string
}

/** A JIT funding schedule stored by the Composable CoW poller. */
export type ComposableCowPollerSchedule = ComposableCowPollerScheduleKey & {
readonly staticInput: string
}

export enum ProofLocation {
// The location of the proofs is private to the caller.
PRIVATE = 0,
Expand Down
105 changes: 105 additions & 0 deletions packages/composable/tests/ComposableCowPoller.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { setGlobalAdapter } from '@cowprotocol/sdk-common'

import {
ComposableCowPollerAbi,
type ComposableCowPollerSchedule,
encodePollFunds,
encodeRegister,
encodeRevoke,
getScheduleId,
} from '../src'
import { createAdapters } from './setup'

const SCHEDULE: ComposableCowPollerSchedule = {
handler: '0x1111111111111111111111111111111111111111',
funder: '0x2222222222222222222222222222222222222222',
owner: '0x3333333333333333333333333333333333333333',
salt: '0x0000000000000000000000000000000000000000000000000000000000000001',
staticInput: '0x1234',
}

const SCHEDULE_ID = '0x7b1516d117fa5dd96fddfb9489b52af1c3cca64e1bc88c32324bdd6a92c6057c'
const REGISTER_CALLDATA =
'0x80313a250000000000000000000000000000000000000000000000000000000000000020000000000000000000000000111111111111111111111111111111111111111100000000000000000000000022222222222222222222222222222222222222220000000000000000000000003333333333333333333333333333333333333333000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000'
const POLL_FUNDS_CALLDATA = '0xf83740307b1516d117fa5dd96fddfb9489b52af1c3cca64e1bc88c32324bdd6a92c6057c'
const REVOKE_CALLDATA = '0xb75c7dc67b1516d117fa5dd96fddfb9489b52af1c3cca64e1bc88c32324bdd6a92c6057c'

describe('ComposableCowPoller - Multi-Adapter Tests', () => {
const adapters = createAdapters()

test('derives the Solidity schedule ID across adapters', () => {
for (const adapter of Object.values(adapters)) {
setGlobalAdapter(adapter)
expect(getScheduleId(SCHEDULE)).toEqual(SCHEDULE_ID)
}
})

test('does not include staticInput in the schedule ID', () => {
const updatedStaticInput = { ...SCHEDULE, staticInput: '0xdeadbeef' }

for (const adapter of Object.values(adapters)) {
setGlobalAdapter(adapter)
expect(getScheduleId(updatedStaticInput)).toEqual(SCHEDULE_ID)
}
})

test('encodes final poller calls across adapters', () => {
for (const adapter of Object.values(adapters)) {
setGlobalAdapter(adapter)
expect(encodeRegister(SCHEDULE)).toEqual(REGISTER_CALLDATA)
expect(encodePollFunds(SCHEDULE_ID)).toEqual(POLL_FUNDS_CALLDATA)
expect(encodeRevoke(SCHEDULE_ID)).toEqual(REVOKE_CALLDATA)
}
Comment thread
igorroncevic marked this conversation as resolved.
})

test('exports the combined poller interface, not the removed topUp method', () => {
const functionNames = ComposableCowPollerAbi.filter((item) => item.type === 'function')
.map((item) => item.name)
.sort()
const errorNames = ComposableCowPollerAbi.filter((item) => item.type === 'error')
.map((item) => item.name)
.sort()
const eventNames = ComposableCowPollerAbi.filter((item) => item.type === 'event')
.map((item) => item.name)
.sort()
const constructor = ComposableCowPollerAbi.find((item) => item.type === 'constructor')
const register = ComposableCowPollerAbi.find((item) => item.type === 'function' && item.name === 'register')
const scheduleId = ComposableCowPollerAbi.find((item) => item.type === 'function' && item.name === 'scheduleId')
const funded = ComposableCowPollerAbi.find((item) => item.type === 'function' && item.name === 'funded')
const scheduleRevoked = ComposableCowPollerAbi.find(
(item) => item.type === 'event' && item.name === 'ScheduleRevoked',
)

expect(functionNames).toEqual([
'COMPOSABLE_COW',
'COW_SHED_FACTORY',
'funded',
'pollFunds',
'register',
'revoke',
'scheduleId',
'schedules',
])
expect(errorNames).toEqual(['NoSchedule', 'OrderNotLive', 'UnauthorizedCaller'])
expect(eventNames).toEqual(['Pulled', 'ScheduleRegistered', 'ScheduleRevoked'])
expect(constructor).toEqual({
inputs: [
{ internalType: 'contract ComposableCoW', name: 'composableCow', type: 'address' },
{ internalType: 'contract ICowShedFactory', name: 'cowShedFactory', type: 'address' },
],
stateMutability: 'nonpayable',
type: 'constructor',
})
expect(scheduleId?.inputs).toEqual(register?.inputs)
expect(funded?.inputs).toEqual([
{ internalType: 'bytes32', name: '', type: 'bytes32' },
{ internalType: 'bytes32', name: '', type: 'bytes32' },
])
expect(funded?.outputs).toEqual([{ internalType: 'bool', name: '', type: 'bool' }])
expect(scheduleRevoked?.inputs).toEqual([
{ indexed: true, internalType: 'bytes32', name: 'id', type: 'bytes32' },
{ indexed: true, internalType: 'address', name: 'owner', type: 'address' },
{ indexed: true, internalType: 'address', name: 'funder', type: 'address' },
])
})
})
Loading