tanglepaysdk-l1tol2
Version:
iota shimmer l1 to l2 demo
538 lines (482 loc) • 17.2 kB
text/typescript
import {
init,
Client,
CoinType,
initLogger,
SHIMMER_TESTNET_BECH32_HRP,
IBasicOutputBuilderOptions,
IBuildBlockOptions
} from '@iota/client-wasm/web';
import type {
AddressTypes,
FeatureTypes,
HexEncodedString,
INativeToken,
UnlockConditionTypes,
ADDRESS_UNLOCK_CONDITION_TYPE,
BASIC_OUTPUT_TYPE,
Ed25519Seed,
ED25519_ADDRESS_TYPE,
ED25519_SIGNATURE_TYPE,
Ed25519Address,
IKeyPair,
ISignatureUnlock,
SIGNATURE_UNLOCK_TYPE,
TRANSACTION_ESSENCE_TYPE,
TRANSACTION_PAYLOAD_TYPE,
ITransactionEssence,
ITransactionPayload,
IUTXOInput,
UTXO_INPUT_TYPE,
IBasicOutput,
IBlock,
IOutputResponse,
INftOutput,
ICommonOutput,
OutputTypes,
} from '@iota/types';
const DEFAULT_PROTOCOL_VERSION = 2;
import {
ACCOUNTS_CONTRACT,
CONTRACT_ADDRESS,
CONTRACT_ALIAS_ID,
EMPTY_BUFFER,
EMPTY_BUFFER_BYTE_LENGTH,
ENDING_SIGNAL_BYTE,
EXTERNALLY_OWNED_ACCOUNT,
EXTERNALLY_OWNED_ACCOUNT_TYPE_ID,
GAS_BUDGET,
TRANSFER_ALLOWANCE,
} from './constant';
import { Allowance, CONTRACT_FUNCTIONS, ILayer2Allowance, ILayer2Parameters, ILayer2TransferAllowanceMetadata, NativeTokenAmount, TARGET_CONTRACTS, TOKEN_ID_BYTE_LENGTH } from './types';
interface Assets {
nativeTokens?: INativeToken[];
nftId?: HexEncodedString;
}
import { WriteStream, Converter, ReadStream } from '@iota/util.js';
import { convertDateToUnixTimestamp, decimalToHex } from './util';
import BigInteger from 'big-integer';
class L1ToL2 {
private _client:Client|undefined;
private _fromAddressHex: string|undefined;
private _fromAddressBech32: string|undefined;
private _mnemonic: string|undefined;
constructor(){
}
async setup(path?:string){
await init(path ? path : './client_wasm_bg.wasm');
await initLogger();
this._client = new Client({
nodes: ['https://api.testnet.shimmer.network'],
localPow: true,
});
}
private _addGasBudget(rawAmount: string): string {
const bigAmount = BigInteger(rawAmount).add(GAS_BUDGET);
return bigAmount.toString();
}
private _encodeSmartContractParameters(
parameters: [string, string][],
): Uint8Array {
const encodedParameters = new WriteStream();
encodedParameters.writeUInt32('parametersLength', parameters.length);
for (const parameter of parameters) {
const [key, value] = parameter;
const keyBytes = Converter.utf8ToBytes(key);
encodedParameters.writeUInt16('keyLength', key.length);
encodedParameters.writeBytes('keyBytes', keyBytes.length, keyBytes);
const valueBytes = Converter.hexToBytes(value);
encodedParameters.writeUInt32('valueLength', valueBytes.length);
encodedParameters.writeBytes('valueBytes', valueBytes.length, valueBytes);
}
return encodedParameters.finalBytes();
}
private _getSecretManager(){
return {
mnemonic:this._mnemonic!,
};
};
setMnemonic(mnemonic:string){
this._mnemonic = mnemonic
}
async getOutputForSend(
amount: string,
){
const targetAmount = BigInteger(amount);
const outputs = await this.getUnspentOutputs();
if (!outputs) return;
for (const outputResp of outputs) {
const output = outputResp.output;
const resAmount = BigInteger(output.amount);
if (resAmount.geq(targetAmount)) {
return outputResp;
}
}
return undefined;
};
async getOutputForNftSend(
nftId: string
){
const outputs = await this.getNftOutputs();
if (!outputs) return;
for (const outputResp of outputs) {
if ((outputResp.output as INftOutput ).nftId === nftId) {
return outputResp;
}
}
}
async getNftOutputs():Promise<IOutputResponse[]|undefined>{
if (!this._client) return
const outputIdsResponse = await this._client.nftOutputIds([
{ address:this._fromAddressBech32??'' },
]);
let addressOutputs = await this._client.getOutputs(outputIdsResponse);
console.log('all nft outputs',addressOutputs)
return addressOutputs;
}
async getUnspentOutputs():Promise<IOutputResponse[]|undefined>{
if (!this._client) return
const outputIdsResponse = await this._client.basicOutputIds([
{ address:this._fromAddressBech32??'' },
{ hasExpiration: false },
{ hasTimelock: false },
{ hasStorageDepositReturn: false },
]);
// Get outputs by their IDs
let addressOutputs = await this._client.getOutputs(outputIdsResponse);
console.log('all outputs',addressOutputs)
// Filter out spent outputs
addressOutputs = addressOutputs.filter(o=>!o.metadata.isSpent)
console.log('unspent outputs',addressOutputs)
return addressOutputs;
}
async prepareAddress(){
if (this._fromAddressBech32 == undefined) {
const secretManager = this._getSecretManager();
const addresses = await this._client?.generateAddresses(secretManager, {
accountIndex: 0,
range: {
start: 0,
end: 1,
},
});
console.log('address',addresses)
this._fromAddressBech32 = addresses? addresses[0]:undefined;
if (this._fromAddressBech32) this._fromAddressHex = await this._client?.bech32ToHex(this._fromAddressBech32)
}
}
private _encodeAddress(address: string): string {
const encodedAddress = new WriteStream();
encodedAddress.writeUInt8(
'Address Type ID',
EXTERNALLY_OWNED_ACCOUNT_TYPE_ID,
);
const addressBytes = Converter.hexToBytes(address);
for (let i = 0; i < addressBytes.length; i++) {
encodedAddress.writeUInt8('Address byte', addressBytes[i]);
}
return encodedAddress.finalHex();
}
private _getLayer2MetadataForTransfer(
layer2Address: string,
rawAmount: string,
nativeTokenId?: string,
surplus?: string,
): string {
const metadataStream = new WriteStream();
metadataStream.writeUInt32('senderContract', EXTERNALLY_OWNED_ACCOUNT);
metadataStream.writeUInt32('targetContract', ACCOUNTS_CONTRACT);
metadataStream.writeUInt32('contractFunction', TRANSFER_ALLOWANCE);
metadataStream.writeUInt64('gasBudget', GAS_BUDGET);
const encodedAddress = this._encodeAddress(layer2Address.toLowerCase());
const smartContractParameters = Object.entries({ a: encodedAddress });
const parameters = this._encodeSmartContractParameters(smartContractParameters);
metadataStream.writeBytes(
'smartContractParameters',
parameters.length,
parameters,
);
const allowance = this._encodeAllowance(rawAmount, nativeTokenId, surplus);
metadataStream.writeBytes('allowance', allowance.length, allowance);
metadataStream.writeUInt16('end', ENDING_SIGNAL_BYTE);
const metadata = '0x' + metadataStream.finalHex();
return metadata;
}
private _encodeAllowance(
rawAmount: string,
nativeTokenId?: string,
surplus?: string,
): Uint8Array {
const allowance = new WriteStream();
const tokenBuffer = new WriteStream();
//if (transactionDetails.type === NewTransactionType.TokenTransfer) {
if (true) {
allowance.writeUInt8('encodedAllowance', Allowance.Set);
if (nativeTokenId == undefined) {
allowance.writeUInt64('iotaAmount', BigInteger(rawAmount));
allowance.writeUInt16('noTokens', EMPTY_BUFFER_BYTE_LENGTH);
allowance.writeUInt16('emptyTokenBuffer', EMPTY_BUFFER);
} else {
allowance.writeUInt64('iotaAmount', BigInteger(surplus ?? '0'));
tokenBuffer.writeUInt16('amountOfTokens', 1);
const tokenIdBytes = Converter.hexToBytes(nativeTokenId.substring(2));
tokenBuffer.writeBytes('tokenId', tokenIdBytes.length, tokenIdBytes);
tokenBuffer.writeUInt256('amount', BigInteger(rawAmount));
const tokenBufferBytes = tokenBuffer.finalBytes();
allowance.writeUInt16('tokensLength', tokenBufferBytes.length);
allowance.writeBytes(
'tokenBuffer',
tokenBufferBytes.length,
tokenBufferBytes,
);
}
}
return allowance.finalBytes();
}
private _getAmountFromTransactionDetails({rawAmount,nftId,nativeTokenId,surplus}:{
rawAmount: string;
nftId?: string;
nativeTokenId?: string;
surplus?: string;
}){
if (!nftId) {
if (nativeTokenId) {
rawAmount = surplus ?? '0'
} else {
rawAmount = BigInt(rawAmount).toString()
}
} else if (nftId) {
rawAmount = surplus ?? '0'
} else {
rawAmount = '0'
}
return rawAmount ?? '0';
}
public async getOutputOptions(
senderAddress: AddressTypes,
recipientAddress: string,
rawAmount: string,
ext: {
nativeTokenId?: string;
metadata?: HexEncodedString;
tag?: string;
giftStorageDeposit?: boolean;
surplus?: string;
layer2Parameters?: ILayer2Parameters;
nftId?: string;
nftOutput?: INftOutput;
expirationDate?: Date;
},
): Promise<IBasicOutput | INftOutput> {
// if (!this._client) throw new Error('client not init')
let {
nativeTokenId,
metadata,
tag,
giftStorageDeposit,
surplus,
layer2Parameters,
nftId,
nftOutput,
expirationDate,
} = ext;
const unixTime = expirationDate
? convertDateToUnixTimestamp(expirationDate)
: undefined;
let amount = this._getAmountFromTransactionDetails({rawAmount,nftId,nativeTokenId,surplus});
amount = layer2Parameters ? this._addGasBudget(amount) : amount;
const bigAmount = BigInteger(rawAmount);
if (tag != undefined) {
tag = Converter.utf8ToHex(tag, true);
}
metadata = layer2Parameters ? this._getLayer2MetadataForTransfer(
recipientAddress,
rawAmount,
nativeTokenId,
surplus,
) : (metadata ? Converter.utf8ToHex(metadata, true) : metadata);
recipientAddress = layer2Parameters ? await this._client!.bech32ToHex(layer2Parameters.networkAddress) : recipientAddress;
const assets: Assets = {};
if (nftId) {
assets.nftId = nftId;
} else if (nativeTokenId) {
assets.nativeTokens = [
{
id: nativeTokenId,
amount: '0x' + bigAmount.toString(16),
},
];
}
const features: FeatureTypes[] = [];
if (metadata) {
features.push({ type: 2, data: metadata });
}
if (layer2Parameters) {
features.push({ type: 0, address: senderAddress });
}
if (tag) {
features.push({ type: 3, tag });
}
const unlockConditions: UnlockConditionTypes[] = [{type:0,address:{type:8,aliasId:
CONTRACT_ALIAS_ID
}}];
if (unixTime) {
unlockConditions.push({ type: 2, unixTime });
}
if (nftId && nftOutput) return {
type:6,
amount:this._addGasBudget(nftOutput.amount),
nftId,
immutableFeatures:nftOutput.immutableFeatures,
features,
unlockConditions,
};
return {
type:3,
amount,
features,
unlockConditions,
};
}
async sendTransaction(
toAddr: string,
amount: string,
nftId?: string,
){
if (!(this._client && this._fromAddressBech32)) return;
let nftOutput:IOutputResponse|undefined
if (nftId) {
nftOutput = await this.getOutputForNftSend(nftId);
}
const outputDetail = await this.getOutputForSend(amount);
if (outputDetail == undefined) return;
const totalFunds = BigInteger(outputDetail.output.amount);
const amountToSend = BigInteger(amount);
const inputs: IUTXOInput[] = [];
inputs.push({
type: 0,
transactionId: outputDetail.metadata.transactionId,
transactionOutputIndex: outputDetail.metadata.outputIndex,
});
if (nftOutput) {
inputs.push({
type: 0,
transactionId: nftOutput.metadata.transactionId,
transactionOutputIndex: nftOutput.metadata.outputIndex,
});
}
const outputs: OutputTypes[] = [];
const basicOutput: IBasicOutput | INftOutput = await this.getOutputOptions(
{ type: 0, pubKeyHash: this._fromAddressHex??'' },
toAddr,
amount,
{
nftId,
nftOutput: nftOutput?.output as INftOutput,
layer2Parameters: {
networkAddress: CONTRACT_ADDRESS,
},
},
);
console.log('receiveroutputs',basicOutput)
outputs.push(basicOutput);
if (totalFunds.gt(amountToSend)) {
// The remaining output that remains in the origin address
let remainingFund = totalFunds.minus(BigInteger(basicOutput.amount))
const remainderBasicOutput: IBasicOutput = {
type: 3,
amount: remainingFund.toString(),
nativeTokens: [],
unlockConditions: [
{
type: 0,
address: {
type: 0,
pubKeyHash: this._fromAddressHex??'',
},
},
],
features: [],
};
outputs.push(remainderBasicOutput);
}
console.log(outputs)
const secretManager = this._getSecretManager();
const blockOption:IBuildBlockOptions = { inputs, outputs }
console.log(blockOption)
const preparedTransactionData = await this._client.prepareTransaction(
secretManager,
blockOption,
);
console.log(preparedTransactionData)
const transactionPayload = (await this._client.signTransaction(
secretManager,
preparedTransactionData,
)) as ITransactionPayload;
console.log(transactionPayload)
const [blockId,block] = await this._client.postBlockPayload(transactionPayload);
console.log(blockId,block);
}
ed2bech32(address:HexEncodedString){
this._client?.hexToBech32(address,'rms')
}
parseLayer2MetadataForTransfer(metadataHex: string): ILayer2TransferAllowanceMetadata {
const metadata = Converter.hexToBytes(metadataHex)
const readStream = new ReadStream(metadata)
const senderContract = readStream.readUInt32('senderContract')
const targetContract = readStream.readUInt32('targetContract')
const contractFunction = readStream.readUInt32('contractFunction')
const gasBudget = readStream.readUInt64('gasBudget')
const smartContractParameters = this._parseSmartContractParameters(readStream)
const ethereumAddress = '0x' + smartContractParameters['a'].substring(2)
const allowance = this._parseAllowance(readStream)
return {
senderContract: decimalToHex(senderContract, true),
targetContract: TARGET_CONTRACTS[targetContract] ?? decimalToHex(targetContract, true),
contractFunction: CONTRACT_FUNCTIONS[contractFunction] ?? decimalToHex(contractFunction, true),
gasBudget: gasBudget.toString(),
ethereumAddress,
baseTokenAmount: allowance?.baseTokenAmount,
nativeTokens: allowance?.nativeTokens,
}
}
private _parseSmartContractParameters(readStream: ReadStream): Record<string, string> {
const smartContractParametersAmount = readStream.readUInt32('parametersLength')
const smartContractParameters: Record<string, string> = {}
for (let index = 0; index < smartContractParametersAmount; index++) {
const keyLength = readStream.readUInt16('keyLength')
const keyBytes = readStream.readBytes('keyValue', keyLength)
const valueLength = readStream.readUInt32('valueLength')
const valueBytes = readStream.readBytes('valueBytes', valueLength)
const key = Converter.bytesToUtf8(keyBytes)
const value = Converter.bytesToHex(valueBytes)
smartContractParameters[key] = value
}
return smartContractParameters
}
private _parseAllowance(readStream: ReadStream): ILayer2Allowance {
const allowance = readStream.readUInt8('allowance')
if (allowance === Allowance.Set) {
const baseTokenAmount = readStream.readUInt64('baseTokenAmount').toString()
readStream.readUInt16('tokenBufferBytesLength')
const tokenAmount = readStream.readUInt16('tokenAmount')
const nativeTokens: NativeTokenAmount[] = []
for (let token = 0; token < tokenAmount; token++) {
const tokenId = Converter.bytesToHex(readStream.readBytes('tokenId', TOKEN_ID_BYTE_LENGTH))
const amount = readStream.readUInt256('tokenAmount').toString()
nativeTokens.push({ tokenId, amount })
}
return {
baseTokenAmount,
nativeTokens,
}
} else {
//@ts-ignore
return
}
}
}
const instance = new L1ToL2
//@ts-ignore
window.l1tol2 = instance
export default instance