From 10127ecfa5a904173d91e811b83bad2d8e3dc328 Mon Sep 17 00:00:00 2001 From: ezra ripps Date: Mon, 2 Feb 2026 14:12:17 -0500 Subject: [PATCH 1/3] add operation not supported errors --- src/services/asset-movement/client.test.ts | 84 ++++++++++++++- src/services/asset-movement/common.ts | 114 ++++++++++++++++++++- 2 files changed, 196 insertions(+), 2 deletions(-) diff --git a/src/services/asset-movement/client.test.ts b/src/services/asset-movement/client.test.ts index 11ff3a97..8b1f1e4f 100644 --- a/src/services/asset-movement/client.test.ts +++ b/src/services/asset-movement/client.test.ts @@ -5,11 +5,13 @@ import { createNodeAndClient } from '../../lib/utils/tests/node.js'; import type { ServiceMetadataExternalizable } from '../../lib/resolver.js'; import KeetaAnchorResolver from '../../lib/resolver.js'; import { type KeetaAnchorAssetMovementServerConfig, KeetaNetAssetMovementAnchorHTTPServer } from './server.js'; -import { Errors, type RailWithExtendedDetails, type KeetaAssetMovementAnchorCreatePersistentForwardingRequest, type KeetaAssetMovementAnchorCreatePersistentForwardingResponse, type KeetaAssetMovementAnchorGetTransferStatusResponse, type KeetaAssetMovementAnchorInitiateTransferClientRequest, type KeetaAssetMovementAnchorInitiateTransferRequest, type KeetaAssetMovementAnchorInitiateTransferResponse, type KeetaAssetMovementAnchorlistPersistentForwardingTransactionsResponse, type KeetaAssetMovementAnchorlistTransactionsRequest, type KeetaAssetMovementTransaction, type ProviderSearchInput } from './common.js'; +import { Errors, type RailWithExtendedDetails, type KeetaAssetMovementAnchorCreatePersistentForwardingRequest, type KeetaAssetMovementAnchorCreatePersistentForwardingResponse, type KeetaAssetMovementAnchorGetTransferStatusResponse, type KeetaAssetMovementAnchorInitiateTransferClientRequest, type KeetaAssetMovementAnchorInitiateTransferRequest, type KeetaAssetMovementAnchorInitiateTransferResponse, type KeetaAssetMovementAnchorlistPersistentForwardingTransactionsResponse, type KeetaAssetMovementAnchorlistTransactionsRequest, type KeetaAssetMovementTransaction, type ProviderSearchInput, toAssetPair, AssetOrPair, Rail } from './common.js'; import { Certificate, CertificateBuilder, SharableCertificateAttributes } from '../../lib/certificates.js'; import type { Routes } from '../../lib/http-server/index.js'; import { KeetaAnchorUserValidationError } from '../../lib/error.js'; +const toJSONSerializable = KeetaNet.lib.Utils.Conversion.toJSONSerializable; + const DEBUG = false; const logger = DEBUG ? console : undefined; @@ -74,9 +76,22 @@ test('Asset Movement Anchor Client Test', async function() { asset: 'USD' }, variableFeeBps: 50 + }, + supportedOperations: { + createPersistentForwarding: false, + initiateTransfer: false } }; + function shouldOperationFailExtendedKeetaSend(inputAsset: AssetOrPair): boolean { + const assetPair = toAssetPair(inputAsset); + if (typeof assetPair.from !== 'string' || !(testCurrencyUSDC.comparePublicKey(assetPair.from)) || assetPair.to !== 'USD') { + return(false); + } + + return(true); + } + await using server = new KeetaNetAssetMovementAnchorHTTPServer({ ...(logger ? { logger: logger } : {}), client: { client: client.client, network: client.config.network, networkAlias: client.config.networkAlias }, @@ -132,6 +147,15 @@ test('Asset Movement Anchor Client Test', async function() { throw(new Error('Missing depositAddress in request')); } + + if (shouldOperationFailExtendedKeetaSend(request.asset)) { + throw(new Errors.OperationNotSupported({ + operationName: 'createPersistentForwarding', + forAsset: request.asset, + forRail: extendedKeetaSendDetails.rail + })); + } + return({ address: request.destinationAddress }) @@ -145,6 +169,14 @@ test('Asset Movement Anchor Client Test', async function() { throw(new Error('Recipient is not a string')); } + if (shouldOperationFailExtendedKeetaSend(request.asset)) { + throw(new Errors.OperationNotSupported({ + operationName: 'initiateTransfer', + forAsset: 'USD', + forRail: extendedKeetaSendDetails.rail + })); + } + return({ id: '123', instructionChoices: [{ @@ -412,6 +444,56 @@ test('Asset Movement Anchor Client Test', async function() { } expect(supportedAssetWithDetails.paths[0]?.pair[1].rails.inbound?.[0]).toEqual(extendedKeetaSendDetails); + + for (const [ method, expectedArguments ] of [ + [ + () => testProvider?.createPersistentForwardingAddress({ + asset: { from: testCurrencyUSDC, to: 'USD' }, + destinationLocation: 'chain:keeta:123', + destinationAddress: 'test-address', + sourceLocation: 'bank-account:us' + }), + { + operationName: 'createPersistentForwarding', + forAsset: { from: testCurrencyUSDC, to: 'USD' }, + forRail: 'KEETA_SEND' + } + ], + [ + () => testProvider?.initiateTransfer({ + asset: { from: testCurrencyUSDC, to: 'USD' }, + from: { location: 'bank-account:us' }, + to: { location: 'chain:keeta:123', recipient: 'test-recipient' }, + value: '1000' + }), + { + operationName: 'initiateTransfer', + forAsset: 'USD', + forRail: 'KEETA_SEND' + } + ] + ] as const) { + let error = null; + try { + await method(); + } catch (e) { + error = e; + } + + if (!(error instanceof Errors.OperationNotSupported)) { + throw(new Error('Expected OperationNotSupported error')); + } + + expect(toJSONSerializable({ + operationName: error.operationName, + forAsset: error.forAsset, + forRail: error.forRail + })).toEqual(toJSONSerializable(({ + operationName: expectedArguments.operationName, + forAsset: expectedArguments.forAsset, + forRail: expectedArguments.forRail + }))); + } } }); diff --git a/src/services/asset-movement/common.ts b/src/services/asset-movement/common.ts index 2a3a8fad..cf2e6b82 100644 --- a/src/services/asset-movement/common.ts +++ b/src/services/asset-movement/common.ts @@ -209,6 +209,21 @@ export interface RailWithExtendedDetails { */ variableFeeBps?: number; } + + /** + * Supported operations for this rail + */ + supportedOperations?: { + /** + * Whether this rail supports creating persistent forwarding addresses for (unmanaged) transfers + */ + createPersistentForwarding?: boolean; + + /** + * Whether this rail supports initiating (managed) transfers + */ + initiateTransfer?: boolean; + } } export type RailOrRailWithExtendedDetails = Rail | RailWithExtendedDetails; @@ -1274,9 +1289,101 @@ class KeetaAssetMovementAnchorAdditionalKYCNeededError extends KeetaAnchorUserEr } } + +export interface KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties { + operationName?: OperationNames | undefined; + forAsset?: AssetOrPair | undefined; + forRail?: Rail | undefined; +} + +export const assertKeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties: (input: unknown) => KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties = createAssertEquals(); + +type KeetaAssetMovementAnchorOperationNotSupportedErrorJSON = ReturnType & KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties; + +class KeetaAssetMovementAnchorOperationNotSupportedError extends KeetaAnchorUserError implements KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties { + static override readonly name: string = 'KeetaAssetMovementAnchorOperationNotSupportedError'; + private readonly KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID!: string; + private static readonly KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID = 'b613cd80-57ac-4be5-ad4a-bb8644d50de6'; + + readonly operationName: OperationNames | undefined; + readonly forAsset: AssetOrPair | undefined; + readonly forRail: Rail | undefined; + + constructor(args: KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties, message?: string) { + super(message ?? `Operation ${args.operationName ? `"${args.operationName}" ` : ''}not supported`); + this.statusCode = 400; + + Object.defineProperty(this, 'KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID', { + value: KeetaAssetMovementAnchorOperationNotSupportedError.KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID, + enumerable: false + }); + + this.operationName = args.operationName; + this.forAsset = args.forAsset; + this.forRail = args.forRail; + } + + static isInstance(input: unknown): input is KeetaAssetMovementAnchorOperationNotSupportedError { + return(this.hasPropWithValue(input, 'KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID', KeetaAssetMovementAnchorOperationNotSupportedError.KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID)); + } + + asErrorResponse(contentType: 'text/plain' | 'application/json'): { error: string; statusCode: number; contentType: string } { + const { operationName, forAsset, forRail } = this.toJSON(); + + let message = this.message; + if (contentType === 'application/json') { + message = JSON.stringify({ + ok: false, + name: this.name, + code: 'KEETA_ANCHOR_ASSET_MOVEMENT_OPERATION_NOT_SUPPORTED', + data: { operationName, forAsset, forRail }, + error: this.message + }); + } + + return({ + error: message, + statusCode: this.statusCode, + contentType: contentType + }); + } + + toJSON(): KeetaAssetMovementAnchorOperationNotSupportedErrorJSON { + return({ + ...super.toJSON(), + operationName: this.operationName, + forRail: this.forRail, + forAsset: this.forAsset ? convertAssetOrPairSearchInputToCanonical(this.forAsset) : undefined + }); + } + + static async fromJSON(input: unknown): Promise { + const { message, other } = this.extractErrorProperties(input, this); + + if (!('data' in other)) { + throw(new Error('Invalid KeetaAssetMovementAnchorOperationNotSupportedError JSON: missing data property')); + } + + const parsed = assertKeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties(other.data); + + const error = new this( + { + forAsset: parsed.forAsset, + forRail: parsed.forRail, + operationName: parsed.operationName + }, + message + ); + + error.restoreFromJSON(other); + return(error); + } +} + export const Errors: { KYCShareNeeded: typeof KeetaAssetMovementAnchorKYCShareNeededError; AdditionalKYCNeeded: typeof KeetaAssetMovementAnchorAdditionalKYCNeededError; + OperationNotSupported: typeof KeetaAssetMovementAnchorOperationNotSupportedError; } = { /** * The user is required to share KYC details @@ -1286,5 +1393,10 @@ export const Errors: { /** * The user is required to complete additional KYC steps */ - AdditionalKYCNeeded: KeetaAssetMovementAnchorAdditionalKYCNeededError + AdditionalKYCNeeded: KeetaAssetMovementAnchorAdditionalKYCNeededError, + + /** + * The requested operation is not supported + */ + OperationNotSupported: KeetaAssetMovementAnchorOperationNotSupportedError }; From fd54889a017b204c4712493f06ffd180987081f0 Mon Sep 17 00:00:00 2001 From: ezra ripps Date: Mon, 2 Feb 2026 17:52:17 -0500 Subject: [PATCH 2/3] remvoe operationName --- src/services/asset-movement/client.test.ts | 11 +---------- src/services/asset-movement/common.ts | 13 ++++--------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/services/asset-movement/client.test.ts b/src/services/asset-movement/client.test.ts index 8b1f1e4f..4a4cc13b 100644 --- a/src/services/asset-movement/client.test.ts +++ b/src/services/asset-movement/client.test.ts @@ -150,7 +150,6 @@ test('Asset Movement Anchor Client Test', async function() { if (shouldOperationFailExtendedKeetaSend(request.asset)) { throw(new Errors.OperationNotSupported({ - operationName: 'createPersistentForwarding', forAsset: request.asset, forRail: extendedKeetaSendDetails.rail })); @@ -171,7 +170,6 @@ test('Asset Movement Anchor Client Test', async function() { if (shouldOperationFailExtendedKeetaSend(request.asset)) { throw(new Errors.OperationNotSupported({ - operationName: 'initiateTransfer', forAsset: 'USD', forRail: extendedKeetaSendDetails.rail })); @@ -454,7 +452,6 @@ test('Asset Movement Anchor Client Test', async function() { sourceLocation: 'bank-account:us' }), { - operationName: 'createPersistentForwarding', forAsset: { from: testCurrencyUSDC, to: 'USD' }, forRail: 'KEETA_SEND' } @@ -467,7 +464,6 @@ test('Asset Movement Anchor Client Test', async function() { value: '1000' }), { - operationName: 'initiateTransfer', forAsset: 'USD', forRail: 'KEETA_SEND' } @@ -485,14 +481,9 @@ test('Asset Movement Anchor Client Test', async function() { } expect(toJSONSerializable({ - operationName: error.operationName, forAsset: error.forAsset, forRail: error.forRail - })).toEqual(toJSONSerializable(({ - operationName: expectedArguments.operationName, - forAsset: expectedArguments.forAsset, - forRail: expectedArguments.forRail - }))); + })).toEqual(toJSONSerializable(expectedArguments)); } } }); diff --git a/src/services/asset-movement/common.ts b/src/services/asset-movement/common.ts index cf2e6b82..5d9b7055 100644 --- a/src/services/asset-movement/common.ts +++ b/src/services/asset-movement/common.ts @@ -1291,7 +1291,6 @@ class KeetaAssetMovementAnchorAdditionalKYCNeededError extends KeetaAnchorUserEr export interface KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties { - operationName?: OperationNames | undefined; forAsset?: AssetOrPair | undefined; forRail?: Rail | undefined; } @@ -1305,12 +1304,11 @@ class KeetaAssetMovementAnchorOperationNotSupportedError extends KeetaAnchorUser private readonly KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID!: string; private static readonly KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID = 'b613cd80-57ac-4be5-ad4a-bb8644d50de6'; - readonly operationName: OperationNames | undefined; readonly forAsset: AssetOrPair | undefined; readonly forRail: Rail | undefined; constructor(args: KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties, message?: string) { - super(message ?? `Operation ${args.operationName ? `"${args.operationName}" ` : ''}not supported`); + super(message ?? `Operatio not supported`); this.statusCode = 400; Object.defineProperty(this, 'KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID', { @@ -1318,7 +1316,6 @@ class KeetaAssetMovementAnchorOperationNotSupportedError extends KeetaAnchorUser enumerable: false }); - this.operationName = args.operationName; this.forAsset = args.forAsset; this.forRail = args.forRail; } @@ -1328,7 +1325,7 @@ class KeetaAssetMovementAnchorOperationNotSupportedError extends KeetaAnchorUser } asErrorResponse(contentType: 'text/plain' | 'application/json'): { error: string; statusCode: number; contentType: string } { - const { operationName, forAsset, forRail } = this.toJSON(); + const { forAsset, forRail } = this.toJSON(); let message = this.message; if (contentType === 'application/json') { @@ -1336,7 +1333,7 @@ class KeetaAssetMovementAnchorOperationNotSupportedError extends KeetaAnchorUser ok: false, name: this.name, code: 'KEETA_ANCHOR_ASSET_MOVEMENT_OPERATION_NOT_SUPPORTED', - data: { operationName, forAsset, forRail }, + data: { forAsset, forRail }, error: this.message }); } @@ -1351,7 +1348,6 @@ class KeetaAssetMovementAnchorOperationNotSupportedError extends KeetaAnchorUser toJSON(): KeetaAssetMovementAnchorOperationNotSupportedErrorJSON { return({ ...super.toJSON(), - operationName: this.operationName, forRail: this.forRail, forAsset: this.forAsset ? convertAssetOrPairSearchInputToCanonical(this.forAsset) : undefined }); @@ -1369,8 +1365,7 @@ class KeetaAssetMovementAnchorOperationNotSupportedError extends KeetaAnchorUser const error = new this( { forAsset: parsed.forAsset, - forRail: parsed.forRail, - operationName: parsed.operationName + forRail: parsed.forRail }, message ); From bce87cf26d77c60d7227e8c657f8b99c2ec43dfc Mon Sep 17 00:00:00 2001 From: ezra ripps Date: Wed, 4 Feb 2026 17:44:49 -0500 Subject: [PATCH 3/3] linting --- src/services/asset-movement/client.test.ts | 2 +- src/services/asset-movement/common.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/asset-movement/client.test.ts b/src/services/asset-movement/client.test.ts index 8237c3e8..5290318a 100644 --- a/src/services/asset-movement/client.test.ts +++ b/src/services/asset-movement/client.test.ts @@ -5,7 +5,7 @@ import { createNodeAndClient } from '../../lib/utils/tests/node.js'; import type { ServiceMetadataExternalizable } from '../../lib/resolver.js'; import KeetaAnchorResolver from '../../lib/resolver.js'; import { type KeetaAnchorAssetMovementServerConfig, KeetaNetAssetMovementAnchorHTTPServer } from './server.js'; -import { Errors, type RailWithExtendedDetails, type KeetaAssetMovementAnchorCreatePersistentForwardingRequest, type KeetaAssetMovementAnchorCreatePersistentForwardingResponse, type KeetaAssetMovementAnchorGetTransferStatusResponse, type KeetaAssetMovementAnchorInitiateTransferClientRequest, type KeetaAssetMovementAnchorInitiateTransferRequest, type KeetaAssetMovementAnchorInitiateTransferResponse, type KeetaAssetMovementAnchorlistPersistentForwardingTransactionsResponse, type KeetaAssetMovementAnchorlistTransactionsRequest, type KeetaAssetMovementTransaction, type ProviderSearchInput, toAssetPair, AssetOrPair, Rail } from './common.js'; +import { Errors, type RailWithExtendedDetails, type KeetaAssetMovementAnchorCreatePersistentForwardingRequest, type KeetaAssetMovementAnchorCreatePersistentForwardingResponse, type KeetaAssetMovementAnchorGetTransferStatusResponse, type KeetaAssetMovementAnchorInitiateTransferClientRequest, type KeetaAssetMovementAnchorInitiateTransferRequest, type KeetaAssetMovementAnchorInitiateTransferResponse, type KeetaAssetMovementAnchorlistPersistentForwardingTransactionsResponse, type KeetaAssetMovementAnchorlistTransactionsRequest, type KeetaAssetMovementTransaction, type ProviderSearchInput, toAssetPair, type AssetOrPair } from './common.js'; import { Certificate, CertificateBuilder, SharableCertificateAttributes } from '../../lib/certificates.js'; import type { Routes } from '../../lib/http-server/index.js'; import { KeetaAnchorUserValidationError } from '../../lib/error.js'; diff --git a/src/services/asset-movement/common.ts b/src/services/asset-movement/common.ts index 74187ea7..97896631 100644 --- a/src/services/asset-movement/common.ts +++ b/src/services/asset-movement/common.ts @@ -1308,7 +1308,7 @@ class KeetaAssetMovementAnchorOperationNotSupportedError extends KeetaAnchorUser readonly forRail: Rail | undefined; constructor(args: KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties, message?: string) { - super(message ?? `Operatio not supported`); + super(message ?? `Operation not supported`); this.statusCode = 400; Object.defineProperty(this, 'KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID', {