Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
24 changes: 19 additions & 5 deletions src/wallet-account-with-protocols.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,16 @@ import { IWalletAccount, NotImplementedError } from '@tetherto/wdk-wallet'
/** @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 {typeof import('@tetherto/wdk-wallet/protocols').SdaProtocol} SdaProtocolCtor */
Comment thread
Davi0kProgramsThings marked this conversation as resolved.
Outdated

/**
* 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 +52,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 {SwapProtocolCtor | BridgeProtocolCtor | LendingProtocolCtor | FiatProtocolCtor | SwidgeProtocolCtor | SdaProtocolCtor} 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 +116,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