@radixdlt/atom-transaction-mapping
Version:
323 lines (293 loc) • 8.87 kB
text/typescript
import { toAddress } from '../../account/test/address.test'
import {
anyUpParticle,
particleGroups,
Spin,
spunParticle,
spunUpParticle,
TokenDefinitionParticleBase,
Atom,
ParticleGroup,
FixedSupplyTokenDefinitionParticle,
TransferrableTokensParticle,
} from '@radixdlt/atom'
import {
TransferTokensAction,
TransferTokensActionInput,
TransferTokensActionT,
} from '@radixdlt/actions'
import {
Amount,
AmountT,
five,
Granularity,
isAmount,
maxAmount,
one,
three,
two,
} from '@radixdlt/primitives'
import { tokenTransferActionToParticleGroupsMapper } from '../src/toAtom/tokenTransferActionToParticleGroupsMapper'
import {
syncMapAtomToTokenTransfers as mapAtomToTokenTransfers,
pgToTokenTransfer,
} from '../src/fromAtom/atomToTokenTransfersMapper'
import { UInt256 } from '@radixdlt/uint256'
import { TokenTransfer } from '../src/fromAtom/_types'
import { unallocatedTokensParticleFromUnsafe } from '../../atom/test/helpers/utility'
import { ResourceIdentifierT } from '@radixdlt/atom/src/_types'
import {
SpunParticleT,
TransferrableTokensParticleT,
UnallocatedTokensParticleT,
} from '@radixdlt/atom/src/particles/_types'
import { AddressT } from '@radixdlt/account'
describe('AtomToTokenTransfersMapper', () => {
const alice = toAddress(
'9S8khLHZa6FsyGo634xQo9QwLgSHGpXHHW764D5mPYBcrnfZV6RT',
)
const bob = toAddress(
'9S9LHeQNFpNJYqLtTJeAbos1LCC5Q7HBiGwPf2oju3NRq5MBKAGt',
)
const carol = toAddress(
'9S8sKfN3wGyJdfyu9RwWvGKtZqq3R1NaxwT63VXi5dEZ6dUJXLyR',
)
const granularity: Granularity = one
const fixedSupplyTokenDefinitionParticle_ = FixedSupplyTokenDefinitionParticle.create(
{
granularity,
supply: maxAmount,
symbol: 'ALICE',
name: 'AliceCoin',
address: alice,
},
)._unsafeUnwrap()
const aliceCoin = fixedSupplyTokenDefinitionParticle_.resourceIdentifier
type AmountLike = number | AmountT
const makeAmount = (amount: AmountLike): AmountT =>
isAmount(amount)
? amount
: Amount.inSmallestDenomination(UInt256.valueOf(amount))
const makeTTPWithSpin = (
spin: Spin,
resourceIdentifier: ResourceIdentifierT,
owner: AddressT,
amount: AmountLike,
): SpunParticleT<TransferrableTokensParticleT> => {
return spunParticle({
particle: TransferrableTokensParticle.create({
amount: makeAmount(amount),
granularity,
resourceIdentifier: resourceIdentifier,
address: owner,
})._unsafeUnwrap(),
spin: spin,
})
}
const makeUnallocatedTokenParticleWithSpin = (
spin: Spin,
resourceIdentifier: ResourceIdentifierT,
amount: AmountLike,
): SpunParticleT<UnallocatedTokensParticleT> => {
return spunParticle({
particle: unallocatedTokensParticleFromUnsafe({
amount: makeAmount(amount),
granularity,
resourceIdentifier: resourceIdentifier,
})._unsafeUnwrap(),
spin: spin,
})
}
const upTTP = makeTTPWithSpin.bind(null, Spin.UP)
const downTTP = makeTTPWithSpin.bind(null, Spin.DOWN)
type TransferTokensActionInputIsh = Omit<
TransferTokensActionInput,
'resourceIdentifier' | 'amount'
>
const makeTransferWithRRI = (
input: TransferTokensActionInputIsh & { amount: AmountLike },
): ((_: ResourceIdentifierT) => TransferTokensActionT) => (
rri: ResourceIdentifierT,
): TransferTokensActionT =>
TransferTokensAction.create({
...input,
amount: makeAmount(input.amount),
resourceIdentifier: rri,
})
const expectSuccess = <T extends TokenDefinitionParticleBase>(
tokenDefinitionParticle: T,
makeTransfer: (rri: ResourceIdentifierT) => TransferTokensActionT,
consumablesFromAmounts: AmountLike[],
filterTokenTransfersForAcccount: AddressT,
validateTransfers: (tokenTransfers: TokenTransfer[]) => void,
): void => {
const resourceID = tokenDefinitionParticle.resourceIdentifier
const transferAction: TransferTokensActionT = makeTransfer(resourceID)
const actor = transferAction.sender
const transferToPGsMapper = tokenTransferActionToParticleGroupsMapper()
const upTTPs = consumablesFromAmounts
.map(upTTP.bind(null, resourceID, actor))
.map((sp: SpunParticleT<TransferrableTokensParticleT>) =>
sp.eraseToAny(),
)
const upParticles = [
spunUpParticle(tokenDefinitionParticle).eraseToAny(),
].concat(upTTPs)
const groups = transferToPGsMapper
.particleGroupsFromAction({
action: transferAction,
upParticles: upParticles.map((sp) =>
anyUpParticle(sp.particle),
),
addressOfActiveAccount: actor,
})
._unsafeUnwrap()
const particleGroups_ = particleGroups(groups)
const atom_ = Atom.create({ particleGroups: particleGroups_ })
const tokenTransfers = mapAtomToTokenTransfers({
atom: atom_,
addressOfActiveAccount: filterTokenTransfersForAcccount,
})
validateTransfers(tokenTransfers)
}
const validateSingleTransfer = (
expectedSender: AddressT,
expectedRecipient: AddressT,
expectedAmount: AmountT,
expectedResourceIdentifer: ResourceIdentifierT,
): ((_: TokenTransfer[]) => void) => (transfers: TokenTransfer[]): void => {
expect(transfers.length).toBe(1)
const tokenTransfer = transfers[0]
expectAddressesEqual(tokenTransfer.from, expectedSender)
expectAddressesEqual(tokenTransfer.to, expectedRecipient)
expectAmountsEqual(tokenTransfer.tokenAmount.amount, expectedAmount)
expectResourceIdentifiersEqual(
tokenTransfer.tokenAmount.token.resourceIdentifier,
expectedResourceIdentifer,
)
}
it('should work with a single transfer with change', () => {
const amountToTransfer = five
expectSuccess(
fixedSupplyTokenDefinitionParticle_,
makeTransferWithRRI({
amount: amountToTransfer,
from: alice,
to: bob,
}),
[one, two, three],
alice,
validateSingleTransfer(alice, bob, amountToTransfer, aliceCoin),
)
})
it('should work with a single transfer without change', () => {
const amountToTransfer = three
expectSuccess(
fixedSupplyTokenDefinitionParticle_,
makeTransferWithRRI({
amount: amountToTransfer,
from: alice,
to: bob,
}),
[one, two],
alice,
validateSingleTransfer(alice, bob, amountToTransfer, aliceCoin),
)
})
it('should return empty list when filterering on a third party address', () => {
expectSuccess(
fixedSupplyTokenDefinitionParticle_,
makeTransferWithRRI({
amount: three,
from: alice,
to: bob,
}),
[one, two],
carol, // <--- Neither sender nor recipient
(transfers) => {
expect(transfers.length).toBe(0)
},
)
})
it('should return one transfer when filterering on recipient', () => {
const amountToTransfer = three
expectSuccess(
fixedSupplyTokenDefinitionParticle_,
makeTransferWithRRI({
amount: amountToTransfer,
from: alice,
to: bob,
}),
[one, two],
bob, // <-- Recipient instead of sender
validateSingleTransfer(alice, bob, amountToTransfer, aliceCoin),
)
})
const makeUpTTP = upTTP.bind(null, aliceCoin)
const makeDownTTP = downTTP.bind(null, aliceCoin)
it('should fail with a PG containing 3 participants', () => {
const weirdParticleGroup = ParticleGroup.create([
makeUpTTP(alice, five),
makeDownTTP(alice, one),
makeUpTTP(bob, one),
makeDownTTP(alice, two),
makeUpTTP(carol, two),
])
const transfersResult = pgToTokenTransfer(weirdParticleGroup)
transfersResult.match(
() => {
throw Error('expected error, but got none')
},
(f) =>
expect(f.message).toBe(
'A transfer should have one or two receivers. Unable to parse.',
),
)
})
it('should fail with a PG containing down TTPs from multiple addresses', () => {
const weirdParticleGroup = ParticleGroup.create([
makeDownTTP(alice, one),
makeDownTTP(bob, two),
])
const transfersResult = pgToTokenTransfer(weirdParticleGroup)
transfersResult.match(
() => {
throw Error('expected error, but got none')
},
(f) => expect(f.message).toBe('Incorrect number of senders.'),
)
})
it('should fail with a PG containing an UnallocatedTokenParticle', () => {
const burnActionParticleGroup = ParticleGroup.create([
makeUpTTP(alice, three),
makeDownTTP(alice, three),
makeUnallocatedTokenParticleWithSpin(
Spin.UP,
aliceCoin,
three,
).eraseToAny(),
])
const transfersResult = pgToTokenTransfer(burnActionParticleGroup)
transfersResult.match(
() => {
throw Error('expected error, but got none')
},
(f) =>
expect(f.message).toBe(
'Action seems to be a burn action, which we will omit and not count as a transfer.',
),
)
})
})
const expectAddressesEqual = (lhs: AddressT, rhs: AddressT): void => {
expect(lhs.toString()).toBe(rhs.toString())
}
const expectAmountsEqual = (lhs: AmountT, rhs: AmountT): void => {
expect(lhs.toString()).toBe(rhs.toString())
}
const expectResourceIdentifiersEqual = (
lhs: ResourceIdentifierT,
rhs: ResourceIdentifierT,
): void => {
expect(lhs.toString()).toBe(rhs.toString())
}