@vechain/sdk-network
Version:
This module serves as the standard interface connecting decentralized applications (dApps) and users to the VeChainThor blockchain
267 lines (242 loc) • 9.89 kB
text/typescript
import {
Address,
Hex,
HexUInt,
Secp256k1,
Transaction,
type TransactionBody
} from '@vechain/sdk-core';
import {
InvalidDataType,
InvalidSecp256k1PrivateKey,
JSONRPCInvalidParams
} from '@vechain/sdk-errors';
import { RPC_METHODS } from '../../../provider/utils/const/rpc-mapper/rpc-methods';
import {
DelegationHandler,
type SignTransactionOptions,
type ThorClient
} from '../../../thor-client';
import {
type AvailableVeChainProviders,
type TransactionRequestInput
} from '../types';
import { VeChainAbstractSigner } from '../vechain-abstract-signer/vechain-abstract-signer';
/**
* Basic VeChain signer with the private key.
* This signer can be initialized using a private key.
*/
class VeChainPrivateKeySigner extends VeChainAbstractSigner {
/**
* Create a new VeChainPrivateKeySigner.
* A signer can be initialized using a private key.
*
* @param privateKey - The private key of the signer
* @param provider - The provider to connect to
*/
constructor(
private readonly privateKey: Uint8Array,
provider?: AvailableVeChainProviders
) {
// Assert if the transaction can be signed
if (!Secp256k1.isValidPrivateKey(privateKey)) {
throw new InvalidSecp256k1PrivateKey(
`VeChainPrivateKeySigner.constructor()`,
"Invalid private key used to sign initialize the signer. Ensure it's a valid Secp256k1 private key.",
undefined
);
}
// Call the parent constructor
super(provider);
}
/**
* Returns a new instance of this Signer connected to //provider// or detached
* from any Provider if null.
*
* @param provider - The provider to connect to
* @returns a new instance of this Signer connected to //provider// or detached
*/
connect(provider: AvailableVeChainProviders): this {
return new VeChainPrivateKeySigner(this.privateKey, provider) as this;
}
/**
* Get the address checksum of the Signer.
*
* @returns the address checksum of the signer
*/
async getAddress(): Promise<string> {
return Address.checksum(
HexUInt.of(
await Promise.resolve(
Address.ofPrivateKey(this.privateKey).toString()
)
)
);
}
/**
* Signs %%transactionToSign%%, returning the fully signed transaction. This does not
* populate any additional properties with eth_getTransactionCount: RPC_METHODS, p0: (string | undefined)[], args: EIP1193RequestArguments* @param transactionToSign - The transaction to sign
* @returns The fully signed transaction
*/
async signTransaction(
transactionToSign: TransactionRequestInput
): Promise<string> {
// Check the provider (needed to sign the transaction)
if (this.provider === undefined) {
throw new JSONRPCInvalidParams(
'VeChainPrivateKeySigner.signTransaction()',
'Thor provider is not found into the signer. Please attach a Provider to your signer instance.',
{ transactionToSign }
);
}
let gasPayer = DelegationHandler(
await this.provider.wallet?.getGasPayer()
).gasPayerOrNull();
// Override the gasPayer if the transaction has a delegation URL
if (transactionToSign.delegationUrl !== undefined) {
gasPayer = {
gasPayerServiceUrl: transactionToSign.delegationUrl
};
}
// Sign the transaction
return await this._signFlow(
transactionToSign,
gasPayer,
this.provider.thorClient
);
}
/**
* Sends a transaction to the blockchain.
*
* @param {TransactionRequestInput} transactionToSend - The transaction object to be sent.
* This includes all the necessary details such as `to`, `value`, `data`, `gasLimit`, etc.
* @return {Promise<string>} A promise that resolves to the transaction hash as a string
* once the transaction is successfully sent.
* @throws {JSONRPCInvalidParams} Throws an error if the provider is not attached
* to the signer, indicating the signer's inability to send the transaction.
*/
async sendTransaction(
transactionToSend: TransactionRequestInput
): Promise<string> {
// 1 - Get the provider (needed to send the raw transaction)
if (this.provider === undefined) {
throw new JSONRPCInvalidParams(
'VeChainPrivateKeySigner.sendTransaction()',
'Thor provider is not found into the signer. Please attach a Provider to your signer instance.',
{ transactionToSend }
);
}
const provider = this.provider;
// 2 - Sign the transaction
const signedTransaction = await this.signTransaction(transactionToSend);
// 3 - Send the signed transaction
return (await provider.request({
method: RPC_METHODS.eth_sendRawTransaction,
params: [signedTransaction]
})) as string;
}
/**
* Signs a payload.
*
* @param {Uint8Array} payload - The payload to be signed as a byte array
* @return {Promise<string>} - A Promise that resolves to the signature as a string.
*/
async signPayload(payload: Uint8Array): Promise<string> {
const sign = Secp256k1.sign(payload, new Uint8Array(this.privateKey));
// SCP256K1 encodes the recovery flag in the last byte. EIP-191 adds 27 to it.
sign[sign.length - 1] += 27;
return await Promise.resolve(Hex.of(sign).toString());
}
/**
* Signs a transaction internal method
*
* @param transaction - The transaction to sign
* @param gasPayer - The gasPayer to use
* @param thorClient - The ThorClient instance
* @returns The fully signed transaction
* @throws {InvalidSecp256k1PrivateKey, InvalidDataType}
*/
async _signFlow(
transaction: TransactionRequestInput,
gasPayer: SignTransactionOptions | null,
thorClient: ThorClient
): Promise<string> {
// Populate the call, to get proper from and to address (compatible with multi-clause transactions)
const populatedTransaction =
await this.populateTransaction(transaction);
// Sign the transaction
return gasPayer !== null
? await this._signWithGasPayer(
populatedTransaction,
this.privateKey,
thorClient,
gasPayer
)
: Hex.of(
Transaction.of(populatedTransaction).sign(this.privateKey)
.encoded
).toString();
}
/**
* Signs a transaction where the gas fee is paid by a gasPayer.
*
* @param unsignedTransactionBody - The unsigned transaction body to sign.
* @param originPrivateKey - The private key of the origin account.
* @param thorClient - The ThorClient instance.
* @param gasPayerOptions - Optional parameters for the request. Includes the `gasPayerServiceUrl` and `gasPayerPrivateKey` fields.
* Only one of the following options can be specified: `gasPayerServiceUrl`, `gasPayerPrivateKey`.
* @returns A promise that resolves to the signed transaction.
* @throws {NotDelegatedTransaction}
*/
private async _signWithGasPayer(
unsignedTransactionBody: TransactionBody,
originPrivateKey: Uint8Array,
thorClient: ThorClient,
gasPayerOptions?: SignTransactionOptions
): Promise<string> {
// Address of the origin account
const originAddress = Address.ofPrivateKey(originPrivateKey).toString();
const unsignedTx = Transaction.of(unsignedTransactionBody);
// Sign transaction with origin private key and gasPayer private key
if (gasPayerOptions?.gasPayerPrivateKey !== undefined) {
// Validate the gas payer private key before using it
if (!HexUInt.isValid(gasPayerOptions.gasPayerPrivateKey)) {
throw new InvalidDataType(
'VeChainPrivateKeySigner._signWithGasPayer',
'Invalid gas payer private key. Ensure it is a valid hexadecimal string.',
{ gasPayerPrivateKey: gasPayerOptions.gasPayerPrivateKey }
);
}
return Hex.of(
Transaction.of(unsignedTransactionBody).signAsSenderAndGasPayer(
originPrivateKey,
HexUInt.of(gasPayerOptions.gasPayerPrivateKey).bytes
).encoded
).toString();
}
// Otherwise, get the signature of the gasPayer from the gasPayer endpoint
const gasPayerSignature = await DelegationHandler(
gasPayerOptions
).getDelegationSignatureUsingUrl(
unsignedTx,
originAddress,
thorClient.httpClient
);
// Sign transaction with origin private key
const originSignature = Secp256k1.sign(
unsignedTx.getTransactionHash().bytes,
originPrivateKey
);
// Sign the transaction with both signatures. Concat both signatures to get the final signature
const signature = new Uint8Array(
originSignature.length + gasPayerSignature.length
);
signature.set(originSignature);
signature.set(gasPayerSignature, originSignature.length);
// Return new signed transaction
return Hex.of(
Transaction.of(unsignedTx.body, signature).encoded
).toString();
}
}
export { VeChainPrivateKeySigner };