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

[client] Adds signWithEckoWallet and quicksignWithEckoWallet functions #750

Merged
merged 6 commits into from
Aug 29, 2023
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
7 changes: 7 additions & 0 deletions .changeset/neat-kiwis-own.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@kadena/client': minor
---

Add `createEckoWalletSign()` and `createEckoWalletQuicksign()`. This creates a
wrapper for the
[eckoWALLET API](https://docs.ecko.finance/eckodex/getting-started/eckowallet/eckowallet-api)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@kadena/client",
"comment": "Adds signWithEckoWallet and quickSignWithEckoWallet",
"type": "minor"
}
],
"packageName": "@kadena/client"
}
24 changes: 24 additions & 0 deletions packages/libs/client/etc/client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export { ChainId }
// @public
export const createClient: ICreateClient;

// @public
export function createEckoWalletQuicksign(): IEckoSignFunction;

// @public
export function createEckoWalletSign(): IEckoSignSingleFunction;

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

Expand Down Expand Up @@ -92,6 +98,16 @@ export { ICommand }

export { ICommandResult }

// @public
export interface ICommonEckoFunctions {
// (undocumented)
connect: (networkId: string) => Promise<boolean>;
// (undocumented)
isConnected: (networkId: string) => Promise<boolean>;
// (undocumented)
isInstalled: () => boolean;
}

// @public
export interface IContinuationPayloadObject {
// (undocumented)
Expand All @@ -113,6 +129,14 @@ export interface ICreateClient {
}) => string): IClient;
}

// @public
export interface IEckoSignFunction extends ISignFunction, ICommonEckoFunctions {
}

// @public
export interface IEckoSignSingleFunction extends ISingleSignFunction, ICommonEckoFunctions {
}

// @public
export interface IExecutionPayloadObject {
// (undocumented)
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[];
}
4 changes: 2 additions & 2 deletions packages/libs/client/src/signing/ISignFunction.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ICommand, IUnsignedCommand } from '@kadena/types';

/**
* Interface to use when writing a singing function that accepts multiple transactions
* Interface to use when writing a signing function that accepts a single transaction
* @public
*/
export interface ISingleSignFunction {
(transaction: IUnsignedCommand): Promise<ICommand | IUnsignedCommand>;
}

/**
* Interface to use when writing a singing function that accepts multiple transactions
* Interface to use when writing a signing function that accepts multiple transactions
* @public
*/
export interface ISignFunction extends ISingleSignFunction {
Expand Down
47 changes: 47 additions & 0 deletions packages/libs/client/src/signing/eckoWallet/eckoCommon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
ICommonEckoFunctions,
IEckoConnectOrStatusResponse,
} from './eckoTypes';

export const isInstalled: ICommonEckoFunctions['isInstalled'] = () => {
const { kadena } = window;
return Boolean(kadena && kadena.isKadena && kadena.request);
};

export const isConnected: ICommonEckoFunctions['isConnected'] = async (
networkId,
) => {
if (!isInstalled()) {
return false;
}

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

return checkStatusResponse?.status === 'success';
};

export const connect: ICommonEckoFunctions['connect'] = async (networkId) => {
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,
});
Comment on lines +37 to +40
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this ever throw?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only when request doesn't exist on window.kadena, which is checked in isConnected. I've now added an extra check in that function to check if window.kadena.request is there.


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

return true;
};
71 changes: 71 additions & 0 deletions packages/libs/client/src/signing/eckoWallet/eckoTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ICommand } from '@kadena/types';

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

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

/**
* Interface that describes the common functions to be used with Ecko Wallet
* @public
*/
export interface ICommonEckoFunctions {
alber70g marked this conversation as resolved.
Show resolved Hide resolved
isInstalled: () => boolean;
isConnected: (networkId: string) => Promise<boolean>;
connect: (networkId: string) => Promise<boolean>;
}
/**
* Interface to use when writing a signing function for Ecko Wallet that accepts a single transaction
* @public
*/
export interface IEckoSignSingleFunction
extends ISingleSignFunction,
ICommonEckoFunctions {}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mixing a function with properties might lead to a confusing api for users since function itself has some methods like apply, call ,...
what if we instead add the sign as a method like this

export interface IEckoSignFunction {
   isInstalled: typeof isInstalled;
   isConnected: typeof isConnected;
   connect: typeof connect;
   sign : ISingleFunction,
   quickSign : ISingleFunction,
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've thought about that, but we wanted to make the usage of this function the same as the other functions to allow drop-in replacement.


/**
* Interface to use when writing a signing function for Ecko Wallet that accepts multiple transactions
* @public
*/
export interface IEckoSignFunction
extends ISignFunction,
ICommonEckoFunctions {}

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;
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ICommand, IUnsignedCommand } from '@kadena/types';

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

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

/**
* 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');
}

if ('quickSignData' in eckoResponse) {
eckoResponse.quickSignData.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;

return quicksignWithEckoWallet;
ash-vd marked this conversation as resolved.
Show resolved Hide resolved
}
52 changes: 52 additions & 0 deletions packages/libs/client/src/signing/eckoWallet/signWithEckoWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { pactCommandToSigningRequest } from '../utils/pactCommandToSigningRequest';
import { parseTransactionCommand } from '../utils/parseTransactionCommand';

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

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

/**
* 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;

return signWithEckoWallet;
}
Loading