Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Adds sign transaction functionality to xrp app #130

Merged
merged 6 commits into from
Oct 21, 2024
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
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
Loading