Skip to content

Commit

Permalink
feat: Adds sign transaction functionality to xrp app (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
muzaffarbhat07 authored Oct 21, 2024
1 parent a036014 commit 66749b9
Show file tree
Hide file tree
Showing 15 changed files with 940 additions and 1 deletion.
4 changes: 4 additions & 0 deletions packages/app-xrp/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export class XrpApp {
);
}

public async signTxn(params: operations.ISignTxnParams) {
return this.sdk.runOperation(() => operations.signTxn(this.sdk, params));
}

public async destroy() {
return this.sdk.destroy();
}
Expand Down
1 change: 1 addition & 0 deletions packages/app-xrp/src/operations/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './getPublicKeys';
export * from './getUserVerifiedPublicKey';
export * from './runGetPublicKeys';
export * from './signTxn';
105 changes: 105 additions & 0 deletions packages/app-xrp/src/operations/signTxn/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { ISDK } from '@cypherock/sdk-core';
import {
createStatusListener,
assert,
hexToUint8Array,
uint8ArrayToHex,
createLoggerWithPrefix,
} from '@cypherock/sdk-utils';
import { APP_VERSION } from '../../constants/appId';
import {
SeedGenerationStatus,
SignTxnStatus,
} from '../../proto/generated/types';
import {
assertOrThrowInvalidResult,
getXrpLib,
OperationHelper,
logger as rootLogger,
} from '../../utils';
import { ISignTxnParams, ISignTxnResult, SignTxnEvent } from './types';

export * from './types';

const logger = createLoggerWithPrefix(rootLogger, 'SignTxn');

export const signTxn = async (
sdk: ISDK,
params: ISignTxnParams,
): Promise<ISignTxnResult> => {
assert(params, 'Params should be defined');
assert(params.walletId, 'walletId should be defined');
assert(params.txn, 'txn should be defined');
assert(typeof params.txn === 'object', 'txn should be an object');
assert(
typeof params.txn.rawTxn === 'object',
'txn.rawTxn should be an object',
);
assert(
typeof params.txn.txnHex === 'string',
'txn.txnHex should be a string',
);
assert(params.derivationPath, 'derivationPath should be defined');
assert(
params.derivationPath.length === 5,
'derivationPath should be equal to 5',
);

await sdk.checkAppCompatibility(APP_VERSION);

const { onStatus, forceStatusUpdate } = createStatusListener({
enums: SignTxnEvent,
operationEnums: SignTxnStatus,
seedGenerationEnums: SeedGenerationStatus,
onEvent: params.onEvent,
logger,
});

const helper = new OperationHelper({
sdk,
queryKey: 'signTxn',
resultKey: 'signTxn',
onStatus,
});

const txnBytes = hexToUint8Array(params.txn.txnHex);

await helper.sendQuery({
initiate: {
walletId: params.walletId,
derivationPath: params.derivationPath,
transactionSize: txnBytes.length,
},
});

const { confirmation } = await helper.waitForResult();
assertOrThrowInvalidResult(confirmation);
forceStatusUpdate(SignTxnEvent.CONFIRM);

await helper.sendInChunks(txnBytes, 'txnData', 'dataAccepted');

await helper.sendQuery({
signature: {},
});
const result = await helper.waitForResult();
assertOrThrowInvalidResult(result.signature);

forceStatusUpdate(SignTxnEvent.PIN_CARD);

const signature = uint8ArrayToHex(result.signature.signature);

let serializedTxn: string | undefined;

if (params.serializeTxn) {
const signedTransaction = {
...params.txn.rawTxn,
TxnSignature: signature,
};
serializedTxn = getXrpLib().encode(signedTransaction);
}

return {
signature,
serializedTxn,
};
};
30 changes: 30 additions & 0 deletions packages/app-xrp/src/operations/signTxn/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Payment as PaymentTransaction } from 'xrpl';

export enum SignTxnEvent {
INIT = 0,
CONFIRM = 1,
VERIFY = 2,
PASSPHRASE = 3,
PIN_CARD = 4,
}

export type SignTxnEventHandler = (event: SignTxnEvent) => void;

export interface IUnsignedTransaction {
rawTxn: PaymentTransaction;
txnHex: string;
}

export interface ISignTxnParams {
onEvent?: SignTxnEventHandler;

walletId: Uint8Array;
derivationPath: number[];
txn: IUnsignedTransaction;
serializeTxn?: boolean;
}

export interface ISignTxnResult {
signature: string;
serializedTxn?: string;
}
1 change: 1 addition & 0 deletions packages/app-xrp/src/operations/types.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './runGetPublicKeys/types';
export * from './getUserVerifiedPublicKey/types';
export * from './signTxn/types';
54 changes: 54 additions & 0 deletions packages/app-xrp/src/utils/operationHelper.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ISDK } from '@cypherock/sdk-core';
import { DeviceAppError, DeviceAppErrorType } from '@cypherock/sdk-interfaces';
import { OnStatus } from '@cypherock/sdk-utils';
import { ChunkPayload, ChunkAck } from '../proto/generated/common';
import { DeepPartial, Exact, Query, Result } from '../proto/generated/xrp/core';
import { assertOrThrowInvalidResult, parseCommonError } from './asserts';

Expand Down Expand Up @@ -33,6 +34,8 @@ export class OperationHelper<Q extends QueryKey, R extends ResultKey> {

private readonly onStatus?: OnStatus;

private static readonly CHUNK_SIZE = 5120;

constructor(params: {
sdk: ISDK;
queryKey: Q;
Expand Down Expand Up @@ -64,4 +67,55 @@ export class OperationHelper<Q extends QueryKey, R extends ResultKey> {

return resultData;
}

private static splitIntoChunks(txn: Uint8Array): Uint8Array[] {
const chunks: Uint8Array[] = [];
const totalChunks = Math.ceil(txn.length / OperationHelper.CHUNK_SIZE);

for (let i = 0; i < totalChunks; i += 1) {
const chunk = txn.slice(
i * OperationHelper.CHUNK_SIZE,
i * OperationHelper.CHUNK_SIZE + OperationHelper.CHUNK_SIZE,
);
chunks.push(chunk);
}

return chunks;
}

public async sendInChunks<
RK extends keyof Exclude<Result[R], null | undefined>,
QK extends keyof Exclude<Query[Q], null | undefined>,
>(data: Uint8Array, queryKey: QK, resultKey: RK) {
const chunks = OperationHelper.splitIntoChunks(data);
let remainingSize = data.length;

for (let i = 0; i < chunks.length; i += 1) {
const chunk = chunks[i];
remainingSize -= chunk.length;

const chunkPayload: ChunkPayload = {
chunk,
chunkIndex: i,
totalChunks: chunks.length,
remainingSize,
};

await this.sendQuery({
[queryKey]: {
chunkPayload,
},
});

const result = await this.waitForResult();
assertOrThrowInvalidResult(result[resultKey]);

const { chunkAck } = result[resultKey] as {
chunkAck: ChunkAck;
};

assertOrThrowInvalidResult(chunkAck);
assertOrThrowInvalidResult(chunkAck.chunkIndex === i);
}
}
}
105 changes: 105 additions & 0 deletions packages/app-xrp/tests/03.signTxn/__fixtures__/error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
DeviceAppError,
DeviceAppErrorType,
deviceAppErrorTypeDetails,
} from '@cypherock/sdk-interfaces';
import { ISignTxnTestCase } from './types';
import { queryToUint8Array, resultToUint8Array } from '../__helpers__';
import { ISignTxnParams } from '../../../src';

const commonParams: {
params: ISignTxnParams;
queries: {
name: string;
data: Uint8Array;
}[];
} = {
params: {
walletId: new Uint8Array([
199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, 103, 233, 62,
110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, 86, 128, 26, 3, 187, 121,
64,
]),
derivationPath: [0x80000000 + 44, 0x80000000 + 144, 0x80000000, 0, 0],
txn: {
rawTxn: {
TransactionType: 'Payment',
Account: 'rQGDkQchoJxMSLZR7q9GwvY3iKtDqTUYNQ',
Amount: '5000000',
Destination: 'rEgfV7YeyG4YayQQufxaqDx4aUA93SidLb',
Flags: 0,
NetworkID: undefined,
Sequence: 676674,
Fee: '12',
LastLedgerSequence: 1238396,
SigningPubKey:
'027497533006d024ffb612a2110eb327ccfeed2b752d787c96ab2d3cca425a40e8',
},
txnHex:
'53545800120000220000000024000A5342201B0012E57C6140000000004C4B4068400000000000000C7321027497533006D024FFB612A2110EB327CCFEED2B752D787C96AB2D3CCA425A40E88114FF2BC637244009494C6203505254126638AAD7CD8314A0F766DFCC0B5DDC91E7679C7539590983A41D9F',
},
},
queries: [
{
name: 'Initate query',
data: queryToUint8Array({
signTxn: {
initiate: {
walletId: new Uint8Array([
199, 89, 252, 26, 32, 135, 183, 211, 90, 220, 38, 17, 160, 103,
233, 62, 110, 172, 92, 20, 35, 250, 190, 146, 62, 8, 53, 86, 128,
26, 3, 187, 121, 64,
]),
derivationPath: [
0x80000000 + 44,
0x80000000 + 144,
0x80000000,
0,
0,
],
transactionSize: 120,
},
},
}),
},
],
};

const withUnknownError: ISignTxnTestCase = {
name: 'With unknown error',
...commonParams,
results: [
{
name: 'error',
data: resultToUint8Array({
signTxn: {
commonError: {
unknownError: 1,
},
},
}),
},
],
errorInstance: DeviceAppError,
errorMessage:
deviceAppErrorTypeDetails[DeviceAppErrorType.UNKNOWN_ERROR].message,
};

const withInvalidResult: ISignTxnTestCase = {
name: 'With invalid result',
...commonParams,
results: [
{
name: 'error',
data: new Uint8Array([18, 4, 26, 2, 24, 1]),
},
],
errorInstance: DeviceAppError,
errorMessage:
deviceAppErrorTypeDetails[DeviceAppErrorType.INVALID_MSG_FROM_DEVICE]
.message,
};

const error: ISignTxnTestCase[] = [withUnknownError, withInvalidResult];

export default error;
14 changes: 14 additions & 0 deletions packages/app-xrp/tests/03.signTxn/__fixtures__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { IFixtures } from './types';
import error from './error';
import invalidData from './invalidData';
import invalidArgs from './invalidArgs';
import valid from './valid';

const fixtures: IFixtures = {
valid,
invalidArgs,
error,
invalidData,
};

export default fixtures;
Loading

0 comments on commit 66749b9

Please sign in to comment.