Skip to content

Commit

Permalink
Merge pull request #159 from Juneo-io/dev
Browse files Browse the repository at this point in the history
v0.0.124
  • Loading branch information
alekswaslet authored Jul 16, 2024
2 parents 00e9775 + 2bef577 commit 983350a
Show file tree
Hide file tree
Showing 18 changed files with 394 additions and 142 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "juneojs",
"version": "0.0.123",
"version": "0.0.124",
"description": "Juneo JS Library",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
14 changes: 14 additions & 0 deletions src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,20 @@ export class JuneoClient {
throw new NetworkError(error.message)
})
}

async get (endpoint: string): Promise<AxiosResponse> {
return await axios
.get(endpoint, {
method: 'get',
baseURL: `${this.protocol}://${this.host}`,
headers: HttpHeaders,
responseType: 'json',
responseEncoding: 'utf8'
})
.catch((error) => {
throw new NetworkError(error.message)
})
}
}

export class JsonRpcRequest {
Expand Down
172 changes: 129 additions & 43 deletions src/transaction/builder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { InputError, OutputError, TimeUtils } from '../utils'
import { Secp256k1OutputTypeId } from './constants'
import { Secp256k1Input, type Spendable, TransferableInput, type UserInput } from './input'
import { Secp256k1Output, UserOutput, type Utxo } from './output'
import { StakeableLockedInputTypeId, StakeableLockedOutputTypeId } from './constants'
import { Secp256k1Input, type Spendable, StakeableLockedInput, TransferableInput, type UserInput } from './input'
import {
Secp256k1Output,
StakeableLockedOutput,
TransferableOutput,
type TransferOutput,
UserOutput,
type Utxo
} from './output'
import { type TransactionFee } from './transaction'
import { Address, AssetId } from './types'

Expand Down Expand Up @@ -42,23 +49,23 @@ export function buildTransactionInputs (
// for the outputs so make sure to spend them to avoid losses.
for (let i = 0; i < utxoSet.length && !isGatheringComplete(targetAmounts, gatheredAmounts); i++) {
const utxo = utxoSet[i]
if (utxo.output.typeId !== Secp256k1OutputTypeId) {
if (!('amount' in utxo.output)) {
continue
}
const output = utxo.output as Secp256k1Output
const output = utxo.output as TransferOutput
const locked = output.locktime > now
const stakeable = output.typeId === StakeableLockedOutputTypeId
// output cannot be consumed because it is timelocked
if (output.locktime > now) {
if (locked && !stakeable) {
throw new InputError('cannot consume time locked utxo')
}
const addressIndices = getSignersIndices(signersAddresses, utxo.output.addresses)
const input =
stakeable && locked
? new StakeableLockedInput(output.locktime, output.amount, addressIndices, utxo)
: new Secp256k1Input(output.amount, addressIndices, utxo)
// The utxo will be added as an input in any case
inputs.push(
new TransferableInput(
utxo.transactionId,
utxo.utxoIndex,
utxo.assetId,
new Secp256k1Input(output.amount, getSignersIndices(signersAddresses, utxo.output.addresses), utxo)
)
)
inputs.push(new TransferableInput(utxo.transactionId, utxo.utxoIndex, utxo.assetId, input))
const assetId = utxo.assetId.value
gatheredAmounts.set(assetId, gatheredAmounts.get(assetId)! + BigInt(output.amount))
}
Expand Down Expand Up @@ -119,13 +126,10 @@ export function buildTransactionOutputs (
let outputs: UserOutput[] = []
// adding outputs matching user inputs
for (const input of userInputs) {
outputs.push(
new UserOutput(
new AssetId(input.assetId),
new Secp256k1Output(input.amount, input.locktime, input.threshold, Address.toAddresses(input.addresses)),
false
)
)
const transferOutput = input.stakeable
? new StakeableLockedOutput(input.locktime, input.amount, input.threshold, Address.toAddresses(input.addresses))
: new Secp256k1Output(input.amount, input.locktime, input.threshold, Address.toAddresses(input.addresses))
outputs.push(new UserOutput(new AssetId(input.assetId), transferOutput, false))
const assetId = input.assetId
const amount = spentAmounts.has(assetId) ? spentAmounts.get(assetId)! : BigInt(0)
spentAmounts.set(assetId, amount + BigInt(input.amount))
Expand All @@ -150,20 +154,14 @@ export function buildTransactionOutputs (
if (spent === available) {
continue
}
const stakeable = input.getTypeId() === StakeableLockedInputTypeId
const transferOutput = stakeable
? new StakeableLockedOutput((input as TransferableInput).input.utxo!.output.locktime, available - spent, 1, [
new Address(changeAddress)
])
: new Secp256k1Output(available - spent, BigInt(0), 1, [new Address(changeAddress)])
// adding change output to send remaining value into the change address
outputs.push(
new UserOutput(
input.getAssetId(),
new Secp256k1Output(
available - spent,
// no locktime for the change
BigInt(0),
1,
[new Address(changeAddress)]
),
true
)
)
outputs.push(new UserOutput(input.getAssetId(), transferOutput, true))
// adding the spending of the change output
const amount = spentAmounts.has(assetId) ? spentAmounts.get(assetId)! : BigInt(0)
spentAmounts.set(assetId, amount + available - spent)
Expand All @@ -174,21 +172,109 @@ export function buildTransactionOutputs (
function mergeSecp256k1Outputs (outputs: UserOutput[]): UserOutput[] {
const mergedOutputs: UserOutput[] = []
const spendings = new Map<string, UserOutput>()
for (const output of outputs) {
let key = output.assetId.value
key += output.output.locktime
key += output.output.threshold.toString()
output.output.addresses.sort(Address.comparator)
for (const address of output.output.addresses) {
for (const userOutput of outputs) {
let key = userOutput.output.typeId.toString()
key += userOutput.assetId.value
key += userOutput.output.locktime
key += userOutput.output.threshold
for (const address of userOutput.output.addresses) {
key += address.serialize().toHex()
}
if (spendings.has(key)) {
const out = spendings.get(key)!.output
;(out as Secp256k1Output).amount += (output.output as Secp256k1Output).amount
out.amount += userOutput.output.amount
} else {
mergedOutputs.push(output)
spendings.set(key, output)
mergedOutputs.push(userOutput)
spendings.set(key, userOutput)
}
}
return mergedOutputs
}

export function buildStakeOutputs (stakeData: UserInput, inputs: TransferableInput[]): TransferableOutput[] {
const stake: TransferableOutput[] = []
const targetStake = stakeData.amount
let spentStakeAmount = BigInt(0)
let spentLockedStakeAmount = BigInt(0)
const stakeableLockedInputs: StakeableLockedInput[] = []
for (const transferableInput of inputs) {
const input = transferableInput.input
const remaining = targetStake - spentStakeAmount - spentLockedStakeAmount
const spent = input.amount > remaining ? remaining : input.amount
if (input.typeId === StakeableLockedInputTypeId) {
spentLockedStakeAmount += spent
stakeableLockedInputs.push(input as StakeableLockedInput)
} else {
spentStakeAmount += spent
}
// enough inputs will be used already
if (spentStakeAmount + spentLockedStakeAmount >= targetStake) {
break
}
}
// output for regular stake
if (spentStakeAmount > 0) {
stake.push(
new TransferableOutput(
new AssetId(stakeData.assetId),
new Secp256k1Output(
spentStakeAmount,
stakeData.locktime,
stakeData.threshold,
Address.toAddresses(stakeData.addresses)
)
)
)
}
// output for stake that is stakeable only
if (spentLockedStakeAmount > 0) {
stake.push(...buildStakeStakeableLockedOutputs(stakeData, stakeableLockedInputs, spentLockedStakeAmount))
}
return stake
}

function buildStakeStakeableLockedOutputs (
stakeData: UserInput,
inputs: StakeableLockedInput[],
spentLockedStakeAmount: bigint
): TransferableOutput[] {
const outputs: TransferableOutput[] = []
const locktimesMap = new Map<bigint, StakeableLockedInput[]>()
for (const input of inputs) {
const key = input.locktime
if (!locktimesMap.has(key)) {
locktimesMap.set(key, [])
}
const values = locktimesMap.get(key)
values!.push(input)
}
let totalAmount = BigInt(0)
for (const [locktime, inputs] of locktimesMap) {
let outputAmount = BigInt(0)
for (const input of inputs) {
const remaining = spentLockedStakeAmount - totalAmount
const isLast = input.amount > remaining
const amount = isLast ? remaining : input.amount
totalAmount += amount
outputAmount += amount
if (isLast) {
break
}
}
outputs.push(
new TransferableOutput(
new AssetId(stakeData.assetId),
new StakeableLockedOutput(
locktime,
outputAmount,
stakeData.threshold,
Address.toAddresses(stakeData.addresses)
)
)
)
if (spentLockedStakeAmount - totalAmount < 1) {
break
}
}
return outputs
}
2 changes: 2 additions & 0 deletions src/transaction/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export const EVMInputSize = AddressSize + 8 + AssetIdSize + 8

export const CodecId = 0

export const InvalidTypeId = -1

export const Secp256k1InitialStateFxId = 0x00000000

export const JVMBaseTransactionTypeId = 0x00000000
Expand Down
58 changes: 52 additions & 6 deletions src/transaction/input.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type Blockchain } from '../chain'
import { InputError, JuneoBuffer, ParsingError, type Serializable, SignatureError } from '../utils'
import { AssetIdSize, Secp256k1InputTypeId, TransactionIdSize } from './constants'
import { AssetIdSize, Secp256k1InputTypeId, StakeableLockedInputTypeId, TransactionIdSize } from './constants'
import { type Utxo } from './output'
import { AbstractSignable } from './signature'
import { type Address, AssetId, TransactionId } from './types'
Expand All @@ -13,6 +13,7 @@ export class UserInput {
threshold: number
destinationChain: Blockchain
locktime: bigint
stakeable: boolean

constructor (
assetId: string,
Expand All @@ -21,7 +22,8 @@ export class UserInput {
addresses: string[],
threshold: number,
destinationChain: Blockchain,
locktime: bigint = BigInt(0)
locktime: bigint = BigInt(0),
stakeable: boolean = false
) {
this.assetId = assetId
this.sourceChain = sourceChain
Expand All @@ -33,10 +35,12 @@ export class UserInput {
this.threshold = threshold
this.destinationChain = destinationChain
this.locktime = locktime
this.stakeable = stakeable
}
}

export interface Spendable {
getTypeId: () => number
getAmount: () => bigint
getAssetId: () => AssetId
}
Expand All @@ -55,11 +59,12 @@ export class TransferableInput extends AbstractSignable implements Serializable,
this.input = input
}

getTypeId (): number {
return this.input.typeId
}

getAmount (): bigint {
if (this.input.typeId === Secp256k1InputTypeId) {
return (this.input as Secp256k1Input).amount
}
return BigInt(0)
return this.input.amount
}

getAssetId (): AssetId {
Expand Down Expand Up @@ -114,6 +119,9 @@ export class TransferableInput extends AbstractSignable implements Serializable,
case Secp256k1InputTypeId: {
return Secp256k1Input.parse(data)
}
case StakeableLockedInputTypeId: {
return StakeableLockedInput.parse(data)
}
default: {
throw new ParsingError(`unsupported input type id "${typeId}"`)
}
Expand Down Expand Up @@ -170,3 +178,41 @@ export class Secp256k1Input implements TransactionInput {
return new Secp256k1Input(amount, indices)
}
}

export class StakeableLockedInput implements TransactionInput {
utxo: Utxo | undefined
readonly typeId = StakeableLockedInputTypeId
locktime: bigint
amount: bigint
addressIndices: number[]

constructor (locktime: bigint, amount: bigint, addressIndices: number[], utxo?: Utxo) {
this.utxo = utxo
this.locktime = locktime
this.amount = amount
this.addressIndices = addressIndices
this.addressIndices.sort((a: number, b: number) => {
return a - b
})
}

serialize (): JuneoBuffer {
const buffer = JuneoBuffer.alloc(
// 4 + 8 + 4 + 8 + 4 = 28
28 + 4 * this.addressIndices.length
)
buffer.writeUInt32(this.typeId)
buffer.writeUInt64(this.locktime)
const transferableInput = new Secp256k1Input(this.amount, this.addressIndices, this.utxo)
buffer.write(transferableInput)
return buffer
}

static parse (data: string | JuneoBuffer): StakeableLockedInput {
const reader = JuneoBuffer.from(data).createReader()
reader.readAndVerifyTypeId(StakeableLockedInputTypeId)
const locktime = reader.readUInt64()
const input = Secp256k1Input.parse(reader.readRemaining())
return new StakeableLockedInput(locktime, input.amount, input.addressIndices, input.utxo)
}
}
5 changes: 5 additions & 0 deletions src/transaction/jevm/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
EVMImportTransactionTypeId,
EVMInputSize,
EVMOutputSize,
InvalidTypeId,
TransactionIdSize
} from '../constants'
import { type Spendable, TransferableInput } from '../input'
Expand Down Expand Up @@ -65,6 +66,10 @@ export class EVMInput extends AbstractSignable implements Serializable, Spendabl
this.nonce = nonce
}

getTypeId (): number {
return InvalidTypeId
}

getAmount (): bigint {
return this.amount
}
Expand Down
Loading

0 comments on commit 983350a

Please sign in to comment.