Skip to content

Commit

Permalink
Adds signWithEckoWallet and quicksignWithEckoWallet functions
Browse files Browse the repository at this point in the history
  • Loading branch information
ash-vd committed Aug 7, 2023
1 parent acfba63 commit e7c03ec
Show file tree
Hide file tree
Showing 12 changed files with 713 additions and 21 deletions.
10 changes: 10 additions & 0 deletions packages/libs/client/etc/client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ export { ChainId }
// @public
export const createClient: ICreateClient;

// Warning: (ae-forgotten-export) The symbol "IEckoSignFunction" needs to be exported by the entry point index.d.ts
//
// @public
export function createEckoWalletQuicksign(): IEckoSignFunction;

// Warning: (ae-forgotten-export) The symbol "IEckoSignSingleFunction" needs to be exported by the entry point index.d.ts
//
// @public
export function createEckoWalletSign(): IEckoSignSingleFunction;

// @public
export const createTransaction: (pactCommand: Partial<IPactCommand>) => IUnsignedCommand;

Expand Down
16 changes: 16 additions & 0 deletions packages/libs/client/src/interfaces/ISigningRequest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { ISigningCap } from '@kadena/types';

import { IPactCommand } from './IPactCommand';

export interface ISigningRequest {
code: string;
data?: Record<string, unknown>;
caps: ISigningCap[];
nonce?: string;
chainId?: IPactCommand['meta']['chainId'];
gasLimit?: number;
gasPrice?: number;
ttl?: number;
sender?: string;
extraSigners?: string[];
}
58 changes: 58 additions & 0 deletions packages/libs/client/src/signing/eckoWallet/eckoCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
IEckoAccountsResponse,
IEckoConnectOrStatusResponse,
} from './eckoTypes';

export const isInstalled = (): boolean => {
const { kadena } = window;
return Boolean(kadena && kadena.isKadena);
};

export const isConnected = async (networkId: string): Promise<boolean> => {
if (!isInstalled()) {
return false;
}

const checkStatusResponse =
await window.kadena?.request<IEckoConnectOrStatusResponse>({
method: 'kda_checkStatus',
networkId,
});

if (checkStatusResponse?.status === 'fail') {
return false;
}

return true;
};

export const connect = async (networkId: string): Promise<boolean> => {
if (!isInstalled()) {
throw new Error('Ecko Wallet is not installed');
}

if (await isConnected(networkId)) {
return true;
}

const connectResponse =
await window.kadena?.request<IEckoConnectOrStatusResponse>({
method: 'kda_connect',
networkId,
});

if (connectResponse?.status === 'fail') {
throw new Error('User declined connection');
}

return true;
};

export const getAccountInfo = async (
networkId: string,
): Promise<IEckoAccountsResponse | undefined> => {
return window.kadena?.request<IEckoAccountsResponse>({
method: 'kda_requestAccount',
networkId,
});
};
59 changes: 59 additions & 0 deletions packages/libs/client/src/signing/eckoWallet/eckoTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ICommand } from '@kadena/types';

import { IQuicksignResponseOutcomes } from '../../signing-api/v1/quicksign';

import {
connect,
getAccountInfo,
isConnected,
isInstalled,
} from './eckoCommon';

export type EckoStatus = 'success' | 'fail';

export interface ICommonEckoFunctions {
isInstalled: typeof isInstalled;
isConnected: typeof isConnected;
connect: typeof connect;
getAccountInfo: typeof getAccountInfo;
}

export interface IEckoConnectOrStatusResponse {
status: EckoStatus;
message?: string;
account?: {
account: string;
publicKey: string;
connectedSites: string[];
};
}

export interface IEckoSignResponse {
status: EckoStatus;
signedCmd: ICommand;
}

export interface IEckoQuicksignSuccessResponse {
status: 'success';
quickSignData: IQuicksignResponseOutcomes['responses'];
}

export interface IEckoQuicksignFailResponse {
status: 'fail';
error: string;
}

export type IEckoQuicksignResponse =
| IEckoQuicksignSuccessResponse
| IEckoQuicksignFailResponse;

export interface IEckoAccountsResponse {
status: EckoStatus;
message?: string;
wallet?: {
account: string;
publicKey: string;
connectedSites: string[];
balance: number;
};
}
100 changes: 100 additions & 0 deletions packages/libs/client/src/signing/eckoWallet/quicksignWithEckoWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { ICommand, IUnsignedCommand } from '@kadena/types';

import { ISignFunction } from '../ISignFunction';
import { addSignatures } from '../utils/addSignature';
import { parseTransactionCommand } from '../utils/parseTransactionCommand';

import {
connect,
getAccountInfo,
isConnected,
isInstalled,
} from './eckoCommon';
import { ICommonEckoFunctions, IEckoQuicksignResponse } from './eckoTypes';

interface IEckoSignFunction extends ISignFunction, ICommonEckoFunctions {}

/**
* Creates the quicksignWithWalletConnect function with interface {@link ISingleSignFunction}
*
* @public
*/
export function createEckoWalletQuicksign(): IEckoSignFunction {
const quicksignWithEckoWallet: IEckoSignFunction = (async (
transactionList: IUnsignedCommand | Array<IUnsignedCommand | ICommand>,
) => {
if (transactionList === undefined) {
throw new Error('No transaction(s) to sign');
}
const isList = Array.isArray(transactionList);
const transactions = isList ? transactionList : [transactionList];

const transactionHashes: string[] = [];

const { networkId } = parseTransactionCommand(transactions[0]);

const commandSigDatas = transactions.map((pactCommand) => {
const { cmd, hash } = pactCommand;
const parsedTransaction = parseTransactionCommand(pactCommand);
transactionHashes.push(hash);

if (networkId !== parsedTransaction.networkId) {
throw new Error('Network is not equal for all transactions');
}

return {
cmd,
sigs: parsedTransaction.signers.map((signer, i) => ({
pubKey: signer.pubKey,
sig: pactCommand.sigs[i]?.sig ?? null,
})),
};
});

const eckoResponse = await window.kadena?.request<IEckoQuicksignResponse>({
method: 'kda_requestQuickSign',
data: {
networkId,
commandSigDatas,
},
});

if (!eckoResponse || eckoResponse?.status === 'fail') {
throw new Error('Error signing transaction');
}

const response = {
responses: eckoResponse.quickSignData,
};

if ('responses' in response) {
response.responses.map((signedCommand, i) => {
if (signedCommand.outcome.result === 'success') {
if (signedCommand.outcome.hash !== transactionHashes[i]) {
throw new Error(
`Hash of the transaction signed by the wallet does not match. Our hash: ${transactionHashes[i]}, wallet hash: ${signedCommand.outcome.hash}`,
);
}

const sigs = signedCommand.commandSigData.sigs.filter(
(sig) => sig.sig !== null,
) as { pubKey: string; sig: string }[];

// Add the signature(s) that we received from the wallet to the PactCommand(s)
transactions[i] = addSignatures(transactions[i], ...sigs);
}
});
} else {
throw new Error('Error signing transaction');
}

return isList ? transactions : transactions[0];
}) as IEckoSignFunction;

quicksignWithEckoWallet.isInstalled = isInstalled;
quicksignWithEckoWallet.isConnected = isConnected;
quicksignWithEckoWallet.connect = connect;
quicksignWithEckoWallet.getAccountInfo = getAccountInfo;

return quicksignWithEckoWallet;
}
63 changes: 63 additions & 0 deletions packages/libs/client/src/signing/eckoWallet/signWithEckoWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { ISingleSignFunction } from '../ISignFunction';
import { pactCommandToSigningRequest } from '../utils/pactCommandToSigningRequest';
import { parseTransactionCommand } from '../utils/parseTransactionCommand';

import {
connect,
getAccountInfo,
isConnected,
isInstalled,
} from './eckoCommon';
import { ICommonEckoFunctions, IEckoSignResponse } from './eckoTypes';

declare global {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Window {
kadena?: {
isKadena: boolean;
request<T>(args: unknown): Promise<T>;
};
}
}

interface IEckoSignSingleFunction
extends ISingleSignFunction,
ICommonEckoFunctions {}

/**
* Creates the signWithEckoWallet function with interface {@link ISingleSignFunction}
*
* @remarks
* It is preferred to use the {@link createEckoWalletQuicksign} function
*
* @public
*/
export function createEckoWalletSign(): IEckoSignSingleFunction {
const signWithEckoWallet: IEckoSignSingleFunction = async (transaction) => {
const parsedTransaction = parseTransactionCommand(transaction);
const signingRequest = pactCommandToSigningRequest(parsedTransaction);

await connect(parsedTransaction.networkId);

const response = await window.kadena?.request<IEckoSignResponse>({
method: 'kda_requestSign',
data: {
networkId: parsedTransaction.networkId,
signingCmd: signingRequest,
},
});

if (response?.signedCmd === undefined) {
throw new Error('Error signing transaction');
}

return response.signedCmd;
};

signWithEckoWallet.isInstalled = isInstalled;
signWithEckoWallet.isConnected = isConnected;
signWithEckoWallet.connect = connect;
signWithEckoWallet.getAccountInfo = getAccountInfo;

return signWithEckoWallet;
}
Loading

0 comments on commit e7c03ec

Please sign in to comment.