-
Notifications
You must be signed in to change notification settings - Fork 24
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
Changes from all commits
3f63cfd
befc4c0
889c253
0c8c6ae
89c02a4
8748d4b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
} |
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[]; | ||
} |
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, | ||
}); | ||
|
||
if (connectResponse?.status === 'fail') { | ||
throw new Error('User declined connection'); | ||
} | ||
|
||
return true; | ||
}; |
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 {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ,... export interface IEckoSignFunction {
isInstalled: typeof isInstalled;
isConnected: typeof isConnected;
connect: typeof connect;
sign : ISingleFunction,
quickSign : ISingleFunction,
} There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
} |
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; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this ever throw?
There was a problem hiding this comment.
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 onwindow.kadena
, which is checked inisConnected
. I've now added an extra check in that function to check ifwindow.kadena.request
is there.