@atomiqlabs/chain-starknet
Version:
Starknet specific base implementation
194 lines (177 loc) • 7.24 kB
text/typescript
import {AbstractSigner} from "@atomiqlabs/base";
import {
Account,
DeployAccountContractPayload,
BlockTag,
Invocation,
DeployAccountContractTransaction,
shortString, constants
} from "starknet";
import {calculateHash, toHex} from "../../utils/Utils";
import {
isStarknetTxDeployAccount,
isStarknetTxInvoke, StarknetTransactions,
StarknetTx,
StarknetTxDeployAccount,
StarknetTxInvoke
} from "../chain/modules/StarknetTransactions";
/**
* Starknet signer implementation wrapping a starknet.js {@link Account}, for browser
* based wallet use {@link StarknetBrowserSigner}
*
* @category Wallets
*/
export class StarknetSigner implements AbstractSigner {
/**
* A static message (text message part), which should be signed by the Starknet wallets to generate reproducible entropy. Works when
* wallets use signing with deterministic nonce, such that signature over the same message always yields the
* same signature (same entropy).
*/
private static readonly STARKNET_REPRODUCIBLE_ENTROPY_MESSAGE = "Signing this messages generates a reproducible secret" +
" to be used on %APPNAME%.";
/**
* A static message (warning part), which should be signed by the Starknet wallets to generate reproducible entropy. Works when
* wallets use signing with deterministic nonce, such that signature over the same message always yields the
* same signature (same entropy).
*/
private static readonly STARKNET_REPRODUCIBLE_ENTROPY_WARNING = "PLEASE DOUBLE CHECK THAT YOU ARE ON THE %APPNAME%" +
" WEBSITE BEFORE SIGNING THE MESSAGE, SIGNING THIS MESSAGE ON ANY OTHER WEBSITE MIGHT LEAD TO LOSS OF FUNDS!";
/**
* Returns a SNIP-12 message to be signed for extracting reproducible entropy. Works when wallets use signing with
* deterministic nonce, such that signature over the same message always yields the same signature (same entropy).
*
* @param starknetChainId Starknet chain ID to use for the SNIP-12 message
* @param appName Application name to differentiate reproducible entropy generated across different apps
*/
public static getReproducibleEntropyMessage(starknetChainId: constants.StarknetChainId, appName: string) {
const message = StarknetSigner.STARKNET_REPRODUCIBLE_ENTROPY_MESSAGE.replace(new RegExp("%APPNAME%", 'g'), appName);
const warning = StarknetSigner.STARKNET_REPRODUCIBLE_ENTROPY_WARNING.replace(new RegExp("%APPNAME%", 'g'), appName);
return {
types: {
StarknetDomain: [
{ name: 'name', type: 'shortstring' },
{ name: 'version', type: 'shortstring' },
{ name: 'chainId', type: 'shortstring' },
{ name: 'revision', type: 'shortstring' },
],
Message: [
{ name: 'Message', type: 'string' },
{ name: 'Warning', type: 'string' }
],
},
primaryType: 'Message',
domain: {
name: appName,
version: '1',
chainId: shortString.decodeShortString(starknetChainId),
revision: '1'
},
message: {
'Message': message,
'Warning': warning
}
};
}
type = "AtomiqAbstractSigner" as const;
public readonly isManagingNoncesInternally: boolean;
readonly account: Account;
/**
* Constructs a signer wrapping a starknet.js {@link Account}
*
* @param account
*/
constructor(account: Account);
constructor(account: Account, isManagingNoncesInternally?: boolean);
constructor(account: Account, isManagingNoncesInternally: boolean = false) {
this.account = account;
this.isManagingNoncesInternally = isManagingNoncesInternally;
}
/**
* @inheritDoc
*/
getAddress(): string {
return toHex(this.account.address);
}
/**
* @param tx
* @internal
*/
protected async _signTransaction(tx: StarknetTx): Promise<StarknetTx> {
if(isStarknetTxInvoke(tx)) {
tx.signed = await this.signInvoke(tx);
} else if(isStarknetTxDeployAccount(tx)) {
tx.signed = await this.signDeployAccount(tx);
} else {
throw new Error("Unsupported transaction type!");
}
calculateHash(tx);
return tx;
}
/**
* Signs the provided starknet transaction and returns its signed version
*
* @param tx A starknet transaction to sign
*/
signTransaction?(tx: StarknetTx): Promise<StarknetTx> {
return this._signTransaction(tx);
}
/**
*
* @param tx
* @protected
*/
protected signInvoke(tx: StarknetTxInvoke): Promise<Invocation> {
return this.account.buildInvocation(tx.tx, tx.details);
}
/**
* @param tx
* @protected
*/
protected signDeployAccount(tx: StarknetTxDeployAccount): Promise<DeployAccountContractTransaction> {
return this.account.buildAccountDeployPayload(tx.tx, tx.details);
}
/**
* Signs and sends the provided starknet transaction. Note that onBeforePublish is not really called before the
* tx is sent out in this default case, since this is not supported by the starknet web based wallets
*
* @param tx A transaction to sign and send
* @param onBeforePublish A callback called after the transaction has been sent
*/
async sendTransaction(tx: StarknetTx, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<string> {
if(isStarknetTxInvoke(tx)) {
tx.txId = await this.sendInvoke(tx);
} else if(isStarknetTxDeployAccount(tx)) {
tx.txId = await this.sendDeployAccount(tx);
} else {
throw new Error("Unsupported transaction type!");
}
if(onBeforePublish!=null) await onBeforePublish(tx.txId, StarknetTransactions.serializeTx(tx));
return tx.txId;
}
/**
*
* @param tx
* @protected
*/
protected async sendInvoke(tx: StarknetTxInvoke): Promise<string> {
const result = await this.account.execute(tx.tx, tx.details);
return result.transaction_hash;
}
/**
*
* @param tx
* @protected
*/
protected async sendDeployAccount(tx: StarknetTxDeployAccount): Promise<string> {
const result = await this.account.deployAccount(tx.tx, tx.details);
return result.transaction_hash;
}
/**
* Returns the payload for deploying the signer account's contract on Starknet
*/
async getDeployPayload(): Promise<DeployAccountContractPayload | null> {
const _account: Account & {getDeploymentData?: () => DeployAccountContractPayload} = this.account;
if(_account.getDeploymentData==null) return null;
return _account.getDeploymentData();
}
}