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
75 changes: 74 additions & 1 deletion src/services/asset-movement/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, 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';

const toJSONSerializable = KeetaNet.lib.Utils.Conversion.toJSONSerializable;

const DEBUG = false;
const logger = DEBUG ? console : undefined;

Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -132,6 +147,14 @@ test('Asset Movement Anchor Client Test', async function() {
throw(new Error('Missing depositAddress in request'));
}


if (shouldOperationFailExtendedKeetaSend(request.asset)) {
throw(new Errors.OperationNotSupported({
forAsset: request.asset,
forRail: extendedKeetaSendDetails.rail
}));
}

return({
address: request.destinationAddress
})
Expand All @@ -145,6 +168,13 @@ test('Asset Movement Anchor Client Test', async function() {
throw(new Error('Recipient is not a string'));
}

if (shouldOperationFailExtendedKeetaSend(request.asset)) {
throw(new Errors.OperationNotSupported({
forAsset: 'USD',
forRail: extendedKeetaSendDetails.rail
}));
}

return({
id: '123',
instructionChoices: [{
Expand Down Expand Up @@ -412,6 +442,49 @@ 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'
}),
{
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'
}),
{
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({
forAsset: error.forAsset,
forRail: error.forRail
})).toEqual(toJSONSerializable(expectedArguments));
}
}
});

Expand Down
109 changes: 108 additions & 1 deletion src/services/asset-movement/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1274,9 +1289,96 @@ class KeetaAssetMovementAnchorAdditionalKYCNeededError extends KeetaAnchorUserEr
}
}


export interface KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties {
forAsset?: AssetOrPair | undefined;
forRail?: Rail | undefined;
}

export const assertKeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties: (input: unknown) => KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties = createAssertEquals<KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties>();

type KeetaAssetMovementAnchorOperationNotSupportedErrorJSON = ReturnType<KeetaAnchorUserError['toJSON']> & 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 forAsset: AssetOrPair | undefined;
readonly forRail: Rail | undefined;

constructor(args: KeetaAssetMovementAnchorOperationNotSupportedErrorJSONProperties, message?: string) {
super(message ?? `Operation not supported`);
this.statusCode = 400;

Object.defineProperty(this, 'KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID', {
value: KeetaAssetMovementAnchorOperationNotSupportedError.KeetaAssetMovementAnchorOperationNotSupportedErrorObjectTypeID,
enumerable: false
});

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 { 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: { forAsset, forRail },
error: this.message
});
}

return({
error: message,
statusCode: this.statusCode,
contentType: contentType
});
}

toJSON(): KeetaAssetMovementAnchorOperationNotSupportedErrorJSON {
return({
...super.toJSON(),
forRail: this.forRail,
forAsset: this.forAsset ? convertAssetOrPairSearchInputToCanonical(this.forAsset) : undefined
});
}

static async fromJSON(input: unknown): Promise<KeetaAssetMovementAnchorOperationNotSupportedError> {
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
},
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
Expand All @@ -1286,5 +1388,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
};