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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"test:coverage": "cross-env NODE_OPTIONS=--experimental-vm-modules jest --coverage"
},
"dependencies": {
"@tetherto/wdk-wallet": "^1.0.0-beta.13",
"@tetherto/wdk-wallet": "^1.0.0-beta.15",
"bare-node-runtime": "^1.4.0",
"zod": "^4.4.3"
},
Expand Down
9 changes: 7 additions & 2 deletions src/policy/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ export const OPERATIONS = [
'repay',
'buy',
'sell',
'swidge'
'swidge',
'createDepositAddress',
'renewDepositAddress',
'recoverDepositAddress',
'disableDepositAddress'
]

export const WILDCARD = '*'
Expand All @@ -50,5 +54,6 @@ export const PROTOCOL_METHODS = {
bridge: ['bridge'],
lending: ['supply', 'withdraw', 'borrow', 'repay'],
fiat: ['buy', 'sell'],
swidge: ['swidge']
swidge: ['swidge'],
sda: ['createDepositAddress', 'renewDepositAddress', 'recoverDepositAddress', 'disableDepositAddress']
}
3 changes: 2 additions & 1 deletion src/policy/policy-account-proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const PROTOCOL_GETTERS = [
['getBridgeProtocol', 'bridge'],
['getLendingProtocol', 'lending'],
['getFiatProtocol', 'fiat'],
['getSwidgeProtocol', 'swidge']
['getSwidgeProtocol', 'swidge'],
['getSdaProtocol', 'sda']
]

/**
Expand Down
3 changes: 2 additions & 1 deletion src/policy/policy-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ import {
* @typedef {'sendTransaction' | 'signTransaction' | 'transfer' | 'approve'
* | 'sign' | 'signTypedData' | 'signAuthorization' | 'delegate' | 'revokeDelegation'
* | 'swap' | 'bridge' | 'supply' | 'withdraw' | 'borrow' | 'repay' | 'buy' | 'sell'
* | 'swidge' | '*'} PolicyOperation
* | 'swidge' | 'createDepositAddress' | 'renewDepositAddress'
* | 'recoverDepositAddress' | 'disableDepositAddress' | '*'} PolicyOperation
*/

/**
Expand Down
38 changes: 24 additions & 14 deletions src/wallet-account-with-protocols.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,26 @@
import { IWalletAccount, NotImplementedError } from '@tetherto/wdk-wallet'

/** @typedef {import('@tetherto/wdk-wallet/protocols').ISwapProtocol} ISwapProtocol */
/** @typedef {typeof import('@tetherto/wdk-wallet/protocols').SwapProtocol} SwapProtocolCtor */

/** @typedef {import('@tetherto/wdk-wallet/protocols').IBridgeProtocol} IBridgeProtocol */
/** @typedef {typeof import('@tetherto/wdk-wallet/protocols').BridgeProtocol} BridgeProtocolCtor */

/** @typedef {import('@tetherto/wdk-wallet/protocols').ILendingProtocol} ILendingProtocol */
/** @typedef {typeof import('@tetherto/wdk-wallet/protocols').LendingProtocol} LendingProtocolCtor */

/** @typedef {import('@tetherto/wdk-wallet/protocols').IFiatProtocol} IFiatProtocol */
/** @typedef {typeof import('@tetherto/wdk-wallet/protocols').FiatProtocol} FiatProtocolCtor */

/** @typedef {import('@tetherto/wdk-wallet/protocols').ISwidgeProtocol} ISwidgeProtocol */
/** @typedef {typeof import('@tetherto/wdk-wallet/protocols').SwidgeProtocol} SwidgeProtocolCtor */
/** @typedef {import('@tetherto/wdk-wallet/protocols').ISdaProtocol} ISdaProtocol */

/** @typedef {import('@tetherto/wdk-wallet/protocols').SwapProtocol} SwapProtocol */
/** @typedef {import('@tetherto/wdk-wallet/protocols').BridgeProtocol} BridgeProtocol */
/** @typedef {import('@tetherto/wdk-wallet/protocols').LendingProtocol} LendingProtocol */
/** @typedef {import('@tetherto/wdk-wallet/protocols').FiatProtocol} FiatProtocol */
/** @typedef {import('@tetherto/wdk-wallet/protocols').SwidgeProtocol} SwidgeProtocol */
/** @typedef {import('@tetherto/wdk-wallet/protocols').SdaProtocol} SdaProtocol */

/**
* Interface for wallet accounts that also expose the WDK's protocol-getter
* helpers (`registerProtocol`, `getSwapProtocol`, `getBridgeProtocol`,
* `getLendingProtocol`, `getFiatProtocol`, `getSwidgeProtocol`). The
* concrete shape is materialized at runtime by `wdk.getAccount` /
* `getAccountByPath` after middlewares and protocol getters have been
* installed. See `WdkAccount` for the consumer-facing type that pairs
* `getLendingProtocol`, `getFiatProtocol`, `getSwidgeProtocol`,
* `getSdaProtocol`). The concrete shape is materialized at runtime by
* `wdk.getAccount` / `getAccountByPath` after middlewares and protocol getters
* have been installed. See `WdkAccount` for the consumer-facing type that pairs
* this surface with the underlying `IWalletAccount` shape.
*
* @interface
Expand All @@ -49,7 +48,7 @@ export class IWalletAccountWithProtocols extends IWalletAccount {
* The label must be unique in the scope of the account and the type of protocol (i.e., there can’t be two protocols of the same
* type bound to the same account with the same label).
*
* @template {SwapProtocolCtor | BridgeProtocolCtor | LendingProtocolCtor | FiatProtocolCtor | SwidgeProtocolCtor} P
* @template {typeof SwapProtocol | typeof BridgeProtocol | typeof LendingProtocol | typeof FiatProtocol | typeof SwidgeProtocol | typeof SdaProtocol} P
* @param {string} label - The label.
* @param {P} Protocol - The protocol class.
* @param {ConstructorParameters<P>[1]} config - The protocol configuration.
Expand Down Expand Up @@ -113,4 +112,15 @@ export class IWalletAccountWithProtocols extends IWalletAccount {
getSwidgeProtocol (label) {
throw new NotImplementedError('getSwidgeProtocol(label)')
}

/**
* Returns the SDA protocol with the given label.
*
* @param {string} label - The label.
* @returns {ISdaProtocol} The SDA protocol.
* @throws {Error} If no SDA protocol has been registered on this account with the given label.
*/
getSdaProtocol (label) {
throw new NotImplementedError('getSdaProtocol(label)')
}
}
30 changes: 26 additions & 4 deletions src/wdk.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import WalletManager from '@tetherto/wdk-wallet'

import { SwapProtocol, BridgeProtocol, LendingProtocol, FiatProtocol, SwidgeProtocol } from '@tetherto/wdk-wallet/protocols'
import { SwapProtocol, BridgeProtocol, LendingProtocol, FiatProtocol, SwidgeProtocol, SdaProtocol } from '@tetherto/wdk-wallet/protocols'

import PolicyEngine from './policy/policy-engine.js'

Expand Down Expand Up @@ -74,7 +74,7 @@ export default class WDK {
this._wallets = new Map()

/** @private */
this._protocols = { swap: Object.create(null), bridge: Object.create(null), lending: Object.create(null), fiat: Object.create(null), swidge: Object.create(null) }
this._protocols = { swap: Object.create(null), bridge: Object.create(null), lending: Object.create(null), fiat: Object.create(null), swidge: Object.create(null), sda: Object.create(null) }

/** @private */
this._middlewares = Object.create(null)
Expand Down Expand Up @@ -139,7 +139,7 @@ export default class WDK {
* same type bound to the same blockchain with the same label).
*
* @see {@link IWalletAccountWithProtocols#registerProtocol} to register protocols only for specific accounts.
* @template {typeof SwapProtocol | typeof BridgeProtocol | typeof LendingProtocol | typeof FiatProtocol | typeof SwidgeProtocol} P
* @template {typeof SwapProtocol | typeof BridgeProtocol | typeof LendingProtocol | typeof FiatProtocol | typeof SwidgeProtocol | typeof SdaProtocol} P
Comment thread
Davi0kProgramsThings marked this conversation as resolved.
* @param {string} blockchain - The name of the blockchain the protocol must be bound to. Can be any string (e.g., "ethereum").
* @param {string} label - The label.
* @param {P} Protocol - The protocol class.
Expand All @@ -151,6 +151,10 @@ export default class WDK {
this._protocols.swidge[blockchain] ??= Object.create(null)

this._protocols.swidge[blockchain][label] = { Protocol, config }
} else if (Protocol.prototype instanceof SdaProtocol) {
this._protocols.sda[blockchain] ??= Object.create(null)

this._protocols.sda[blockchain][label] = { Protocol, config }
} else if (Protocol.prototype instanceof SwapProtocol) {
this._protocols.swap[blockchain] ??= Object.create(null)

Expand Down Expand Up @@ -322,13 +326,15 @@ export default class WDK {
_registerProtocols (account, { blockchain }) {
if (this._decoratedAccounts.has(account)) return

const protocols = { swap: Object.create(null), bridge: Object.create(null), lending: Object.create(null), fiat: Object.create(null), swidge: Object.create(null) }
const protocols = { swap: Object.create(null), bridge: Object.create(null), lending: Object.create(null), fiat: Object.create(null), swidge: Object.create(null), sda: Object.create(null) }

this._decoratedAccounts.add(account)

account.registerProtocol = (label, Protocol, config) => {
if (Protocol.prototype instanceof SwidgeProtocol) {
protocols.swidge[label] = new Protocol(account, config)
} else if (Protocol.prototype instanceof SdaProtocol) {
protocols.sda[label] = new Protocol(account, config)
} else if (Protocol.prototype instanceof SwapProtocol) {
protocols.swap[label] = new Protocol(account, config)
} else if (Protocol.prototype instanceof BridgeProtocol) {
Expand Down Expand Up @@ -421,5 +427,21 @@ export default class WDK {

throw new Error(`No swidge protocol registered for label: ${label}.`)
}

account.getSdaProtocol = (label) => {
if (this._protocols.sda[blockchain]?.[label]) {
const { Protocol, config } = this._protocols.sda[blockchain][label]

const protocol = new Protocol(account, config)

return protocol
}

if (protocols.sda[label]) {
return protocols.sda[label]
}

throw new Error(`No sda protocol registered for label: ${label}.`)
}
}
}
71 changes: 70 additions & 1 deletion tests/wdk-policy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, jest, test } from '@jest/globals'

import WalletManager from '@tetherto/wdk-wallet'

import { BridgeProtocol, SwapProtocol, SwidgeProtocol } from '@tetherto/wdk-wallet/protocols'
import { BridgeProtocol, SdaProtocol, SwapProtocol, SwidgeProtocol } from '@tetherto/wdk-wallet/protocols'

import WDK, { PolicyConfigurationError, PolicyViolationError } from '../index.js'

Expand All @@ -19,6 +19,8 @@ const DUMMY_QUOTE = { fee: 1n }
const DUMMY_SWAP_RESULT = { hash: '0xdummy-swap-hash' }
const DUMMY_BRIDGE_RESULT = { hash: '0xdummy-bridge-hash' }
const DUMMY_SWIDGE_RESULT = { hash: '0xdummy-swidge-hash' }
const DUMMY_SDA_ADDRESS_RESULT = [{ address: '0xdummy-deposit-address' }]
const DUMMY_SDA_ROUTES = [{ sourceChains: ['arbitrum'], destinationChain: 'polygon' }]
const DUMMY_SIGNED_TX = '0xdummy-signed-tx'

// Test inputs (no DUMMY_ prefix per CQ5). Addresses are valid EVM shape
Expand Down Expand Up @@ -1798,6 +1800,73 @@ describe('WDK — policy engine', () => {
expect(swidgeInstanceMock).not.toHaveBeenCalled()
})

test('an sda protocol write method (createDepositAddress) is wrapped and blocks on DENY; getSupportedRoutes is not wrapped', async () => {
const createDepositAddressInstanceMock = jest.fn().mockResolvedValue(DUMMY_SDA_ADDRESS_RESULT)
const getSupportedRoutesInstanceMock = jest.fn().mockResolvedValue(DUMMY_SDA_ROUTES)

class MySdaProtocol extends SdaProtocol {
constructor () { super() }
async createDepositAddress (opts) { return createDepositAddressInstanceMock(opts) }
async getSupportedRoutes (opts) { return getSupportedRoutesInstanceMock(opts) }
}

getAccountMock.mockResolvedValue(buildAccount())

wdk
.registerWallet('ethereum', WalletManagerMock, {})
.registerProtocol('ethereum', 'accumulate', MySdaProtocol, {})
.registerPolicy({
id: 'no-sda',
name: 'no-sda',
scope: 'project',
rules: [{ name: 'deny-create-deposit', operation: 'createDepositAddress', action: 'DENY', conditions: [] }]
})

const account = await wdk.getAccount('ethereum', 0)
const sda = account.getSdaProtocol('accumulate')

const denied = await catchAsync(() => sda.createDepositAddress({ sourceChains: ['arbitrum'], destinationChain: 'polygon' }))

expect(denied.name).toBe('PolicyViolationError')
expect(denied.policyId).toBe('no-sda')
expect(denied.ruleName).toBe('deny-create-deposit')
expect(createDepositAddressInstanceMock).not.toHaveBeenCalled()

const routes = await sda.getSupportedRoutes({})
expect(routes).toEqual(DUMMY_SDA_ROUTES)
expect(getSupportedRoutesInstanceMock).toHaveBeenCalledWith({})
})

test('account.simulate.getSdaProtocol(label).createDepositAddress(...) returns a structured DENY without executing', async () => {
const createDepositAddressInstanceMock = jest.fn().mockResolvedValue(DUMMY_SDA_ADDRESS_RESULT)

class MySdaProtocol extends SdaProtocol {
constructor () { super() }
async createDepositAddress (opts) { return createDepositAddressInstanceMock(opts) }
}

getAccountMock.mockResolvedValue(buildAccount())

wdk
.registerWallet('ethereum', WalletManagerMock, {})
.registerProtocol('ethereum', 'accumulate', MySdaProtocol, {})
.registerPolicy({
id: 'no-sda',
name: 'no-sda',
scope: 'project',
rules: [{ name: 'deny-create-deposit', operation: 'createDepositAddress', action: 'DENY', conditions: [] }]
})

const account = await wdk.getAccount('ethereum', 0)
const sim = await account.simulate.getSdaProtocol('accumulate').createDepositAddress({ sourceChains: ['arbitrum'], destinationChain: 'polygon' })

expect(sim.decision).toBe('DENY')
expect(sim.policy_id).toBe('no-sda')
expect(sim.matched_rule).toBe('deny-create-deposit')
expect(sim.reason).toBe('deny-create-deposit')
expect(createDepositAddressInstanceMock).not.toHaveBeenCalled()
})

test('account.registerProtocol(...) returns the proxy so chained calls stay enforced (C-2)', async () => {
// Regression for C-2: without intercepting registerProtocol in the
// proxy's get trap, the underlying _registerProtocols closure hands
Expand Down
Loading