@ckb-ccc/core
Version:
Core of CCC - CKBer's Codebase
260 lines (229 loc) • 7.42 kB
text/typescript
import { Address } from "../../address/index.js";
import { Bytes, BytesLike, bytesConcat, bytesFrom } from "../../bytes/index.js";
import {
Script,
Transaction,
TransactionLike,
WitnessArgs,
} from "../../ckb/index.js";
import { KnownScript } from "../../client/index.js";
import { Hasher, HasherKeecak256 } from "../../hasher/index.js";
import { Hex, HexLike, hexFrom } from "../../hex/index.js";
import { numToBytes } from "../../num/index.js";
import { reduceAsync } from "../../utils/index.js";
import { Signer, SignerSignType, SignerType } from "../signer/index.js";
/**
* An abstract class extending Signer for Ethereum Virtual Machine (EVM) based signing operations.
* This class provides methods to get EVM account, internal address, and signing transactions.
* @public
*/
export abstract class SignerEvm extends Signer {
get type(): SignerType {
return SignerType.EVM;
}
get signType(): SignerSignType {
return SignerSignType.EvmPersonal;
}
/**
* Gets the EVM account associated with the signer.
*
* @returns A promise that resolves to a string representing the EVM account.
*/
abstract getEvmAccount(): Promise<Hex>;
/**
* Gets the internal address, which is the EVM account in this case.
*
* @returns A promise that resolves to a string representing the internal address.
*/
async getInternalAddress(): Promise<string> {
return this.getEvmAccount();
}
/**
* Gets an array of Address objects representing the known script addresses for the signer.
*
* @returns A promise that resolves to an array of Address objects.
*/
async getAddressObjs(): Promise<Address[]> {
const account = await this.getEvmAccount();
const addresses = await Promise.all([
this._getOmniLockAddresses(account),
this._getPWLockAddresses(account),
]);
return addresses.flat();
}
_getOmniLockAddresses(account: HexLike): Promise<Address[]> {
return Promise.all([
this._getOmniLockEvmAddressObj(account),
this._getOmniLockOldEvmAddressObj(account),
]);
}
async _getPWLockAddresses(account: HexLike): Promise<Address[]> {
const addr = await this._getPWLockEvmAddressObj(account);
if (!addr) {
return [];
}
return [addr];
}
async _getOmniLockEvmAddressObj(account: HexLike): Promise<Address> {
return Address.fromKnownScript(
this.client,
KnownScript.OmniLock,
hexFrom([0x12, ...bytesFrom(account), 0x00]),
);
}
async _getOmniLockOldEvmAddressObj(account: HexLike): Promise<Address> {
return Address.fromKnownScript(
this.client,
KnownScript.OmniLock,
hexFrom([0x1, ...bytesFrom(account), 0x00]),
);
}
async _getPWLockEvmAddressObj(
account: HexLike,
): Promise<Address | undefined> {
try {
return Address.fromKnownScript(
this.client,
KnownScript.PWLock,
hexFrom(bytesFrom(account)),
);
} catch {}
return;
}
/**
* prepare a transaction before signing. This method is not implemented and should be overridden by subclasses.
*
* @param txLike - The transaction to prepare, represented as a TransactionLike object.
* @returns A promise that resolves to the prepared Transaction object.
*/
async prepareTransaction(txLike: TransactionLike): Promise<Transaction> {
const tx = Transaction.from(txLike);
if (
(await tx.findInputIndexByLockId(
await this.client.getKnownScript(KnownScript.OmniLock),
this.client,
)) !== undefined
) {
await tx.addCellDepsOfKnownScripts(this.client, KnownScript.OmniLock);
}
if (
(await tx.findInputIndexByLockId(
await this.client.getKnownScript(KnownScript.PWLock),
this.client,
)) !== undefined
) {
await tx.addCellDepsOfKnownScripts(this.client, KnownScript.PWLock);
}
const account = await this.getEvmAccount();
const omniLockAddresses = await this._getOmniLockAddresses(account);
const pwLockAddresses = await this._getPWLockAddresses(account);
const omniTx = reduceAsync(
omniLockAddresses,
(tx: Transaction, { script }) =>
tx.prepareSighashAllWitness(script, 85, this.client),
tx,
);
return reduceAsync(
pwLockAddresses,
(tx: Transaction, { script }) =>
tx.prepareSighashAllWitness(script, 65, this.client),
omniTx,
);
}
/**
* Signs a transaction without modifying it.
*
* @param txLike - The transaction to sign, represented as a TransactionLike object.
* @returns A promise that resolves to a signed Transaction object.
*/
async signOnlyTransaction(txLike: TransactionLike): Promise<Transaction> {
let tx = Transaction.from(txLike);
const account = await this.getEvmAccount();
const { script: evmScript } = await this._getOmniLockEvmAddressObj(account);
const { script: oldEvmScript } =
await this._getOmniLockOldEvmAddressObj(account);
tx = await this._signOmniLockScriptForTransaction(
tx,
evmScript,
(hash) => `CKB transaction: ${hash}`,
);
tx = await this._signOmniLockScriptForTransaction(
tx,
oldEvmScript,
(hash) => bytesFrom(hash),
);
const pwAddress = await this._getPWLockEvmAddressObj(account);
if (pwAddress) {
tx = await this._signPWLockScriptForTransaction(
tx,
pwAddress.script,
(hash) => bytesFrom(hash),
);
}
return tx;
}
async _signOmniLockScriptForTransaction(
tx: Transaction,
script: Script,
messageTransformer: (hash: string) => BytesLike,
): Promise<Transaction> {
const info = await this._signPersonalEvmForTransaction(
tx,
script,
messageTransformer,
);
if (!info) {
return tx;
}
const witness = WitnessArgs.fromBytes(tx.witnesses[info.position]);
witness.lock = hexFrom(
bytesConcat(
numToBytes(5 * 4 + info.signature.length, 4),
numToBytes(4 * 4, 4),
numToBytes(5 * 4 + info.signature.length, 4),
numToBytes(5 * 4 + info.signature.length, 4),
numToBytes(info.signature.length, 4),
info.signature,
),
);
tx.setWitnessArgsAt(info.position, witness);
return tx;
}
async _signPWLockScriptForTransaction(
tx: Transaction,
script: Script,
messageTransformer: (hash: string) => BytesLike,
): Promise<Transaction> {
const info = await this._signPersonalEvmForTransaction(
tx,
script,
messageTransformer,
new HasherKeecak256(),
);
if (!info) {
return tx;
}
const witness = WitnessArgs.fromBytes(tx.witnesses[info.position]);
witness.lock = hexFrom(info.signature);
tx.setWitnessArgsAt(info.position, witness);
return tx;
}
async _signPersonalEvmForTransaction(
tx: Transaction,
script: Script,
messageTransformer: (hash: string) => BytesLike,
hasher?: Hasher,
): Promise<{ signature: Bytes; position: number } | undefined> {
const info = await tx.getSignHashInfo(script, this.client, hasher);
if (!info) {
return;
}
const signature = bytesFrom(
await this.signMessageRaw(messageTransformer(info.message)),
);
if (signature[signature.length - 1] >= 27) {
signature[signature.length - 1] -= 27;
}
return { signature, position: info.position };
}
}