@radixdlt/application
Version:
A JavaScript client library for interacting with the Radix Distributed Ledger.
1,924 lines (1,680 loc) • 45.5 kB
text/typescript
/*
import {
AccountAddress,
AccountAddressT,
ResourceIdentifier,
SigningKeychain,
ValidatorAddress,
} from '@radixdlt/account'
import {
interval,
Observable,
of,
ReplaySubject,
Subject,
Subscription,
throwError,
} from 'rxjs'
import {
filter,
map,
mergeMap,
shareReplay,
skipWhile,
take,
tap,
toArray,
} from 'rxjs/operators'
import {
KeystoreT,
MessageEncryption,
PrivateKey,
PublicKey,
PublicKeyT,
HDMasterSeed,
Mnemonic,
Message,
} from '@radixdlt/crypto'
import {
AccountT,
ActionType,
alice,
APIError,
APIErrorCause,
balancesFor,
bob,
BuiltTransaction,
carol,
ErrorCategory,
ExecutedTransaction,
isStakeTokensAction,
isTransferTokensAction,
isUnstakeTokensAction,
ManualUserConfirmTX,
mockedAPI,
mockRadixCoreAPI,
NodeT,
Radix,
RadixCoreAPI,
RadixT,
SimpleTokenBalances,
TokenBalances,
TransactionIdentifier,
TransactionIdentifierT,
TransactionIntentBuilder,
TransactionStatus,
TransactionTrackingEventType,
TransactionType,
TransferTokensInput,
TransferTokensOptions,
Wallet,
WalletT,
} from '../src'
import { Amount, AmountT, Network } from '@radixdlt/primitives'
import {
log,
LogLevel,
msgFromError,
restoreDefaultLogLevel,
toObservable,
} from '@radixdlt/util'
import { mockErrorMsg } from '../../util/test/util'
import {
ExecutedAction,
ExecutedStakeTokensAction,
ExecutedTransferTokensAction,
ExecutedUnstakeTokensAction,
IntendedAction,
SimpleExecutedTransaction,
TransactionIntent,
} from '../'
import { signatureFromHexStrings } from '@radixdlt/crypto/test/utils'
import { UInt256 } from '@radixdlt/uint256'
import { createWallet, keystoreForTest, makeWalletWithFunds } from './util'
const mockTransformIntentToExecutedTX = (
txIntent: TransactionIntent,
): SimpleExecutedTransaction => {
const txID = TransactionIdentifier.create(
'deadbeef'.repeat(8),
)._unsafeUnwrap()
const mockTransformIntendedActionToExecutedAction = (
intendedAction: IntendedAction,
): ExecutedAction => {
if (isTransferTokensAction(intendedAction)) {
const tokenTransfer: ExecutedTransferTokensAction = {
...intendedAction,
rri: intendedAction.rri,
}
return tokenTransfer
} else if (isStakeTokensAction(intendedAction)) {
const stake: ExecutedStakeTokensAction = {
...intendedAction,
}
return stake
} else if (isUnstakeTokensAction(intendedAction)) {
const unstake: ExecutedUnstakeTokensAction = {
...intendedAction,
}
return unstake
} else {
throw new Error('Missed some action type...')
}
}
const msg = txIntent.message
if (!msg) {
log.info(`TX intent contains no message.`)
}
const executedTx: SimpleExecutedTransaction = {
txID,
sentAt: new Date(Date.now()),
fee: Amount.fromUnsafe(1)._unsafeUnwrap(),
message: msg?.toString('hex'),
actions: txIntent.actions.map(
mockTransformIntendedActionToExecutedAction,
),
}
if (executedTx.message) {
log.info(`Mocked executed TX contains a message.`)
}
return executedTx
}
const dummyNode = (urlString: string): Observable<NodeT> =>
of({
url: new URL(urlString),
})
describe('radix_high_level_api', () => {
it('can load test keystore', async done => {
// keystoreForTest
await SigningKeychain.byLoadingAndDecryptingKeystore({
password: keystoreForTest.password,
load: () => Promise.resolve(keystoreForTest.keystore),
}).match(
signingKeychain => {
const revealedMnemonic = signingKeychain.revealMnemonic()
expect(revealedMnemonic.phrase).toBe(
'legal winner thank year wave sausage worth useful legal winner thank yellow',
)
expect(revealedMnemonic.entropy.toString('hex')).toBe(
'7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f',
)
const masterSeed = HDMasterSeed.fromMnemonic({
mnemonic: revealedMnemonic,
})
expect(masterSeed.seed.toString('hex')).toBe(
'878386efb78845b3355bd15ea4d39ef97d179cb712b77d5c12b6be415fffeffe5f377ba02bf3f8544ab800b955e51fbff09828f682052a20faa6addbbddfb096',
)
done()
},
error => {
done(error)
},
)
})
it('can be created empty', () => {
const radix = Radix.create()
expect(radix).toBeDefined()
})
it('emits node connection without signingKeychain', async done => {
const radix = Radix.create()
radix.__withAPI(mockedAPI)
radix.__node.subscribe(
node => {
expect(node.url.host).toBe('www.radixdlt.com')
done()
},
error => done(error),
)
})
const testChangeNode = async (
expectedValues: string[],
done: jest.DoneCallback,
emitNewValues: (radix: RadixT) => void,
): Promise<void> => {
const radix = Radix.create()
radix.__node
.pipe(
map((n: NodeT) => n.url.toString()),
take(2),
toArray(),
)
.subscribe(
nodes => {
expect(nodes).toStrictEqual(expectedValues)
done()
},
error => done(error),
)
emitNewValues(radix)
}
it('can change node with nodeConnection', async done => {
const n1 = 'https://www.rewards.radixtokens.com/'
const n2 = 'https://www.radixdlt.com/'
await testChangeNode([n1, n2], done, (radix: RadixT) => {
radix.__withNodeConnection(dummyNode(n1))
radix.__withNodeConnection(dummyNode(n2))
})
})
it('can change node to connect to', async done => {
const n1 = 'https://www.rewards.radixtokens.com/'
const n2 = 'https://www.radixdlt.com/'
await testChangeNode([n1, n2], done, (radix: RadixT) => {
radix.__withAPI(of(mockRadixCoreAPI({ nodeUrl: n1 })))
radix.__withAPI(of(mockRadixCoreAPI({ nodeUrl: n2 })))
})
})
it('can observe active account without API', async done => {
const radix = Radix.create()
const wallet = createWallet({ startWithInitialSigningKey: true })
radix.__withWallet(wallet)
radix.activeAccount.subscribe(
account => {
expect(account.hdPath!.addressIndex.value()).toBe(0)
done()
},
error => done(error),
)
})
it('radix can restoreSigningKeysUpToIndex', done => {
const subs = new Subscription()
const radix = Radix.create().__withWallet(
createWallet({ startWithInitialSigningKey: false }),
)
const index = 3
subs.add(
radix.restoreLocalHDAccountsToIndex(index).subscribe(
accounts => {
expect(accounts.size()).toBe(index)
accounts.all.forEach((account: AccountT, idx) => {
expect(account.hdPath!.addressIndex.value()).toBe(idx)
})
done()
},
(e: Error) => {
done(e)
},
),
)
})
it('provides networkId for wallets', async done => {
const radix = Radix.create()
const wallet = createWallet()
radix.__withWallet(wallet)
radix.__withAPI(mockedAPI)
radix.activeAddress.subscribe(
address => {
expect(address.network).toBe(Network.MAINNET)
done()
},
error => done(error),
)
})
it('returns native token without signingKeychain', async done => {
const radix = Radix.create()
radix.__withAPI(mockedAPI)
radix.ledger.nativeToken().subscribe(
token => {
expect(token.symbol).toBe('XRD')
done()
},
error => done(error),
)
})
it('should be able to detect errors', done => {
const invalidURLErrorMsg = 'invalid url'
const failingNode: Observable<NodeT> = throwError(() => {
return new Error(invalidURLErrorMsg)
})
const subs = new Subscription()
const radix = Radix.create()
subs.add(
radix.__node.subscribe(n => {
done(new Error('Expected error but did not get any'))
}),
)
subs.add(
radix.errors.subscribe({
next: error => {
expect(error.category).toEqual(ErrorCategory.NODE)
done()
},
}),
)
radix.__withNodeConnection(failingNode)
})
it('login_with_keystore', async done => {
const radix = Radix.create().__withAPI(mockedAPI)
radix.connect('http://www.test.com')
radix.__wallet.subscribe(
(im: WalletT) => {
const account = im.__unsafeGetAccount()
expect(account.hdPath!.addressIndex.value()).toBe(0)
expect(account.publicKey.toString(true)).toBe(
keystoreForTest.publicKeysCompressed[0],
)
done()
},
e => done(e),
)
const loadKeystore = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
radix.login(keystoreForTest.password, loadKeystore)
})
it('radix can reveal mnemonic', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
radix.connect('http://www.test.com')
subs.add(
radix.revealMnemonic().subscribe(
m => {
expect(m.phrase).toBe(
keystoreForTest.expectedMnemonicPhrase,
)
done()
},
e => {
done(e)
},
),
)
radix.login(keystoreForTest.password, () =>
Promise.resolve(keystoreForTest.keystore),
)
})
describe('radix_api_failing_scenarios', () => {
beforeAll(() => {
log.setLevel('silent')
})
afterAll(() => {
restoreDefaultLogLevel()
})
it('should handle signingKeychain error', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
radix.connect('http://www.test.com')
let haveSeenError = false
subs.add(
radix.__wallet.subscribe(
(wallet: WalletT) => {
const account = wallet.__unsafeGetAccount()
expect(account.hdPath!.addressIndex.value()).toBe(0)
expect(account.publicKey.toString(true)).toBe(
keystoreForTest.publicKeysCompressed[0],
)
expect(haveSeenError).toBe(true)
done()
},
error => done(error),
),
)
subs.add(
radix.errors.subscribe({
next: error => {
haveSeenError = true
expect(error.category).toEqual(ErrorCategory.WALLET)
},
}),
)
const errMsg = mockErrorMsg('LoadError')
const loadKeystoreError = (): Promise<KeystoreT> =>
Promise.reject(new Error(errMsg))
const loadKeystoreSuccess = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
radix.login(keystoreForTest.password, loadKeystoreError)
radix.login(keystoreForTest.password, loadKeystoreSuccess)
})
it('should forward an error when calling api', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(
of({
...mockRadixCoreAPI(),
tokenBalancesForAddress: () =>
throwError(
() =>
new Error(
'error that should trigger expected failure.',
),
),
}),
)
subs.add(
radix.tokenBalances.subscribe({
next: _n => {
done(
new Error(
'Unexpectedly got tokenBalance, but expected error.',
),
)
},
}),
)
subs.add(
radix.errors.subscribe(error => {
expect(error.category).toEqual(ErrorCategory.API)
expect(error.cause).toEqual(
APIErrorCause.TOKEN_BALANCES_FAILED,
)
done()
}),
)
radix.__withWallet(createWallet())
})
it('does not kill property observables when rpc requests fail', async done => {
const subs = new Subscription()
let amountVal = 100
let counter = 0
const api = of(<RadixCoreAPI>{
...mockRadixCoreAPI(),
tokenBalancesForAddress: (
a: AccountAddressT,
): Observable<SimpleTokenBalances> => {
if (counter > 2 && counter < 5) {
counter++
return throwError(() => new Error('Manual error'))
} else {
const observableBalance = of(balancesFor(a, amountVal))
counter++
amountVal += 100
return observableBalance
}
},
})
const radix = Radix.create()
radix.__withWallet(createWallet())
radix.__withAPI(api).withTokenBalanceFetchTrigger(interval(250))
const expectedValues = [100, 200, 300]
subs.add(
radix.tokenBalances
.pipe(
map(tb => tb.tokenBalances[0].amount.valueOf()),
take(expectedValues.length),
toArray(),
)
.subscribe(amounts => {
expect(amounts).toEqual(expectedValues)
done()
}),
)
})
})
it('radix can derive accounts', async done => {
const subs = new Subscription()
const radix = Radix.create()
subs.add(
radix.activeAccount
.pipe(
map(account => account.hdPath!.addressIndex.value()),
take(2),
toArray(),
)
.subscribe(
accounts => {
expect(accounts).toStrictEqual([0, 2])
done()
},
e => done(e),
),
)
radix
.__withWallet(createWallet())
.deriveNextAccount()
.deriveNextAccount({ alsoSwitchTo: true })
})
it('radix can switch to accounts', async done => {
const subs = new Subscription()
const radix = Radix.create()
const expectedValues = [0, 1, 2, 3, 1, 0, 3, 0]
subs.add(
radix.activeAccount
.pipe(
map(account => account.hdPath!.addressIndex.value()),
take(expectedValues.length),
toArray(),
)
.subscribe(
accounts => {
expect(accounts).toStrictEqual(expectedValues)
done()
},
e => done(e),
),
)
const wallet = createWallet({ startWithInitialSigningKey: true })
const firstAccount = wallet.__unsafeGetAccount()
radix
.__withWallet(wallet) //0
.deriveNextAccount({ alsoSwitchTo: true }) // 1
.deriveNextAccount({ alsoSwitchTo: true }) // 2
.deriveNextAccount({ alsoSwitchTo: true }) // 3
.switchAccount({ toIndex: 1 })
.switchAccount('first')
.switchAccount('last')
.switchAccount({ toAccount: firstAccount })
})
it('deriveNextAccount method on radix updates accounts', done => {
const subs = new Subscription()
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(mockedAPI)
const expected = [1, 2, 3]
subs.add(
radix.accounts
.pipe(
map(i => i.size()),
take(expected.length),
toArray(),
)
.subscribe(values => {
expect(values).toStrictEqual(expected)
done()
}),
)
radix.deriveNextAccount({ alsoSwitchTo: true })
radix.deriveNextAccount({ alsoSwitchTo: false })
})
it('deriveNextAccount alsoSwitchTo method on radix updates activeSigningKey', done => {
const subs = new Subscription()
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(mockedAPI)
const expected = [0, 1, 3]
subs.add(
radix.activeAccount
.pipe(
map(account => account.hdPath!.addressIndex.value()),
take(expected.length),
toArray(),
)
.subscribe(values => {
expect(values).toStrictEqual(expected)
done()
}),
)
radix.deriveNextAccount({ alsoSwitchTo: true })
radix.deriveNextAccount({ alsoSwitchTo: false })
radix.deriveNextAccount({ alsoSwitchTo: true })
})
it('deriveNextAccount alsoSwitchTo method on radix updates activeAddress', done => {
const subs = new Subscription()
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(mockedAPI)
const expectedCount = 3
subs.add(
radix.activeAddress
.pipe(take(expectedCount), toArray())
.subscribe(values => {
expect(values.length).toBe(expectedCount)
done()
}),
)
radix.deriveNextAccount({ alsoSwitchTo: true })
radix.deriveNextAccount({ alsoSwitchTo: false })
radix.deriveNextAccount({ alsoSwitchTo: true })
})
it('mocked API returns different but deterministic tokenBalances per account', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
radix.connect('http://www.test.com')
const loadKeystore = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
radix.login(keystoreForTest.password, loadKeystore)
subs.add(
radix.__wallet.subscribe(_w => {
const expectedValues = [
{ pkIndex: 0, tokenBalancesCount: 1 },
{ pkIndex: 1, tokenBalancesCount: 4 },
{ pkIndex: 2, tokenBalancesCount: 1 },
{ pkIndex: 3, tokenBalancesCount: 1 },
{ pkIndex: 4, tokenBalancesCount: 1 },
]
subs.add(
radix.tokenBalances
.pipe(take(expectedValues.length), toArray())
.subscribe(values => {
values.forEach((tb, index: number) => {
const expected = expectedValues[index]
expect(tb.owner.publicKey.toString(true)).toBe(
keystoreForTest.publicKeysCompressed[
expected.pkIndex
],
)
expect(tb.tokenBalances.length).toBe(
expected.tokenBalancesCount,
)
})
done()
}),
)
radix.deriveNextAccount({ alsoSwitchTo: true })
radix.deriveNextAccount({ alsoSwitchTo: true })
radix.deriveNextAccount({ alsoSwitchTo: true })
radix.deriveNextAccount({ alsoSwitchTo: true })
}),
)
})
it('tokenBalances with tokeninfo', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
radix.connect('http://www.test.com')
const loadKeystore = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
radix.login(keystoreForTest.password, loadKeystore)
subs.add(
radix.__wallet.subscribe(_w => {
type ExpectedValue = { name: string; amount: string }
const expectedValues: ExpectedValue[] = [
{ name: 'Bar token', amount: '8138000' },
]
subs.add(
radix.tokenBalances
.pipe(
map((tbs: TokenBalances): ExpectedValue[] => {
return tbs.tokenBalances.map(
(bot): ExpectedValue => ({
name: bot.token.name,
amount: bot.amount.toString(),
}),
)
}),
)
.subscribe(values => {
expect(values).toStrictEqual(expectedValues)
done()
}),
)
}),
)
})
it('mocked API returns different but deterministic transaction history per account', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
radix.connect('http://www.test.com')
const loadKeystore = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
radix.login(keystoreForTest.password, loadKeystore)
subs.add(
radix.__wallet.subscribe(_w => {
const expectedValues = [
{ pkIndex: 0, actionsCountForEachTx: [2, 1, 4] },
{ pkIndex: 1, actionsCountForEachTx: [8, 9, 10] },
{ pkIndex: 2, actionsCountForEachTx: [5, 6, 7] },
]
subs.add(
radix
.transactionHistory({ size: 3 })
.pipe(take(expectedValues.length), toArray())
.subscribe(values => {
values.forEach((txHist, index: number) => {
const expected = expectedValues[index]
txHist.transactions.forEach(tx => {
expect(tx.txID.toString().length).toBe(64)
})
expect(
txHist.transactions.map(
tx => tx.actions.length,
),
).toStrictEqual(expected.actionsCountForEachTx)
})
done()
}),
)
radix.deriveNextAccount({ alsoSwitchTo: true })
radix.deriveNextAccount({ alsoSwitchTo: true })
}),
)
})
it('should handle transaction status updates', done => {
const radix = Radix.create().__withAPI(mockedAPI)
const expectedValues: TransactionStatus[] = [
TransactionStatus.PENDING,
TransactionStatus.CONFIRMED,
]
radix
.transactionStatus(
TransactionIdentifier.create(
'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
)._unsafeUnwrap(),
interval(10),
)
.pipe(
map(({ status }) => status),
take(expectedValues.length),
toArray(),
)
.subscribe(values => {
expect(values).toStrictEqual(expectedValues)
done()
})
})
it('can lookup tx', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
radix.connect('http://www.test.com')
const loadKeystore = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
radix.login(keystoreForTest.password, loadKeystore)
const mockedTXId = TransactionIdentifier.create(
Buffer.from(
'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
'hex',
),
)._unsafeUnwrap()
subs.add(
radix.__wallet.subscribe(_w => {
radix.ledger.lookupTransaction(mockedTXId).subscribe(tx => {
expect(tx.txID.equals(mockedTXId)).toBe(true)
expect(tx.actions.length).toBeGreaterThan(0)
done()
})
}),
)
})
it('can lookup validator', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
radix.connect('http://www.test.com')
const loadKeystore = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
radix.login(keystoreForTest.password, loadKeystore)
const mockedValidatorAddr = ValidatorAddress.fromUnsafe(
'tv1qdqft0u899axwce955fkh9rundr5s2sgvhpp8wzfe3ty0rn0rgqj2x6y86p',
)._unsafeUnwrap()
subs.add(
radix.__wallet.subscribe(_w => {
radix.ledger
.lookupValidator(mockedValidatorAddr)
.subscribe(validator => {
expect(
validator.address.equals(mockedValidatorAddr),
).toBe(true)
expect(
validator.ownerAddress.toString().slice(-4),
).toBe('j7dt')
done()
})
}),
)
})
it('should get validators', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
subs.add(
radix.ledger
.validators({
size: 10,
cursor: '',
})
.subscribe(validators => {
expect(validators.validators.length).toEqual(10)
done()
}),
)
})
it('should get build transaction response', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
const transactionIntent = TransactionIntentBuilder.create()
.stakeTokens({
validator:
'tv1qdqft0u899axwce955fkh9rundr5s2sgvhpp8wzfe3ty0rn0rgqj2x6y86p',
amount: 10000,
})
.__syncBuildDoNotEncryptMessageIfAny(alice)
._unsafeUnwrap()
subs.add(
radix.ledger
.buildTransaction(transactionIntent, undefined as any)
.subscribe(unsignedTx => {
expect(
(unsignedTx as { fee: AmountT }).fee.toString(),
).toEqual('14370')
done()
}),
)
})
it('should get finalizeTransaction response', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
subs.add(
radix.ledger
.finalizeTransaction({
publicKeyOfSigner: alice.publicKey,
transaction: {
blob: 'xyz',
hashOfBlobToSign: 'deadbeef',
},
signature: signatureFromHexStrings({
r:
'934b1ea10a4b3c1757e2b0c017d0b6143ce3c9a7e6a4a49860d7a6ab210ee3d8',
s:
'2442ce9d2b916064108014783e923ec36b49743e2ffa1c4496f01a512aafd9e5',
}),
})
.subscribe(pendingTx => {
expect(
(pendingTx as {
txID: TransactionIdentifierT
}).txID.toString(),
).toEqual(
'3608bca1e44ea6c4d268eb6db02260269892c0b42b86bbf1e77a6fa16c3c9282',
)
done()
}),
)
})
it('should get network transaction demand response', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
subs.add(
radix.ledger.NetworkTransactionDemand().subscribe(result => {
expect(result.tps).toEqual(109)
done()
}),
)
})
it('should get network transaction throughput response', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
subs.add(
radix.ledger.NetworkTransactionThroughput().subscribe(result => {
expect(result.tps).toEqual(10)
done()
}),
)
})
it('can fetch stake positions', done => {
const subs = new Subscription()
const radix = Radix.create()
.__withAPI(mockedAPI)
.withStakingFetchTrigger(interval(100))
radix.connect('http://www.test.com')
const loadKeystore = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
radix.login(keystoreForTest.password, loadKeystore)
const expectedStakes = [38, 22, 8]
const expectedValues = [expectedStakes, expectedStakes] // should be unchanged between updates (deterministically mocked).
subs.add(
radix.__wallet.subscribe(_w => {
radix.stakingPositions
.pipe(
map(sp => sp.map(p => p.amount.valueOf() % 100)),
take(expectedValues.length),
toArray(),
)
.subscribe(values => {
expect(values).toStrictEqual(expectedValues)
done()
})
}),
)
})
it('can fetch unstake positions', done => {
const subs = new Subscription()
const radix = Radix.create()
.__withAPI(mockedAPI)
.withStakingFetchTrigger(interval(50))
radix.connect('http://www.test.com')
const loadKeystore = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
radix.login(keystoreForTest.password, loadKeystore)
const expectedStakes = [
{ amount: 138, validator: '6p', epochsUntil: 0 },
{ amount: 722, validator: '6p', epochsUntil: 0 },
{ amount: 208, validator: '6p', epochsUntil: 3 },
]
const expectedValues = [expectedStakes, expectedStakes] // should be unchanged between updates (deterministically mocked).
subs.add(
radix.__wallet.subscribe(_w => {
radix.unstakingPositions
.pipe(
map(sp =>
sp.map(p => ({
amount: p.amount.valueOf() % 1000,
validator: p.validator.toString().slice(-2),
epochsUntil: p.epochsUntil,
})),
),
take(expectedValues.length),
toArray(),
)
.subscribe(values => {
expect(values).toStrictEqual(expectedValues)
done()
})
}),
)
})
it('can attach messages to transfers and skip encrypting them', async done => {
const subs = new Subscription()
let receivedMsg = 'not_set'
const plaintext =
'Hey Bob, this is Alice, you and I can read this message, but no one else.'
const errNoMessage = 'found_no_message_in_tx'
const mockedAPI = mockRadixCoreAPI()
const radix = Radix.create()
.__withWallet(makeWalletWithFunds(Network.MAINNET))
.__withAPI(
of({
...mockedAPI,
buildTransaction: (
txIntent: TransactionIntent,
): Observable<BuiltTransaction> => {
receivedMsg = txIntent.message
? Message.plaintextToString(txIntent.message)
: errNoMessage
return mockedAPI.buildTransaction(
txIntent,
undefined as any,
)
},
}),
)
subs.add(
radix
.transferTokens({
transferInput: {
to: bob,
amount: 1,
tokenIdentifier: 'xrd_tr1qyf0x76s',
},
userConfirmation: 'skip',
message: { plaintext, encrypt: false },
})
.completion.subscribe({
next: _ => {
expect(receivedMsg).toBe(plaintext)
done()
},
error: e => {
done(
new Error(
`Got error, but expected none: ${msgFromError(
e,
)}`,
),
)
},
}),
)
})
it('can attach messages to transfers and encrypt them', async done => {
const subs = new Subscription()
let receivedMsgHex = 'not_set'
const alicePrivateKey = PrivateKey.fromScalar(
UInt256.valueOf(1),
)._unsafeUnwrap()
const alicePublicKey = alicePrivateKey.publicKey()
const bobPrivateKey = PrivateKey.fromScalar(
UInt256.valueOf(2),
)._unsafeUnwrap()
const bobPublicKey = bobPrivateKey.publicKey()
const bob = AccountAddress.fromPublicKeyAndNetwork({
publicKey: bobPublicKey,
network: Network.MAINNET,
})
const plaintext =
'Hey Bob, this is Alice, you and I can read this message, but no one else.'
const errNoMessage = 'found_no_message_in_tx'
const mockedAPI = mockRadixCoreAPI()
const radix = Radix.create()
.__withWallet(makeWalletWithFunds(Network.MAINNET))
.__withAPI(
of({
...mockedAPI,
buildTransaction: (
txIntent: TransactionIntent,
): Observable<BuiltTransaction> => {
receivedMsgHex =
txIntent.message?.toString('hex') ?? errNoMessage
return mockedAPI.buildTransaction(
txIntent,
undefined as any,
)
},
}),
)
const transactionTracking = radix.transferTokens({
transferInput: {
to: bob,
amount: 1,
tokenIdentifier: 'xrd_tr1qyf0x76s',
},
userConfirmation: 'skip',
message: { plaintext, encrypt: true },
})
subs.add(
transactionTracking.completion
.pipe(
mergeMap(
(_): Observable<Buffer> => {
return toObservable(
MessageEncryption.decrypt({
encryptedMessage: Buffer.from(
receivedMsgHex,
'hex',
),
diffieHellmanPoint: alicePrivateKey.diffieHellman.bind(
null,
bobPublicKey,
),
}),
)
},
),
tap((decryptedByAlice: Buffer) => {
expect(decryptedByAlice.toString('utf8')).toBe(
plaintext,
)
}),
mergeMap(
(_): Observable<Buffer> => {
return toObservable(
MessageEncryption.decrypt({
encryptedMessage: Buffer.from(
receivedMsgHex,
'hex',
),
diffieHellmanPoint: bobPrivateKey.diffieHellman.bind(
null,
alicePublicKey,
),
}),
)
},
),
map((decryptedByBob: Buffer) => {
return decryptedByBob.toString('utf8')
}),
)
.subscribe({
next: decryptedByBob => {
expect(decryptedByBob).toBe(plaintext)
done()
},
error: e => {
done(
new Error(
`Got error, but expected none: ${msgFromError(
e,
)}`,
),
)
},
}),
)
})
it('can decrypt encrypted message in transaction', done => {
const subs = new Subscription()
const radix = Radix.create().__withAPI(mockedAPI)
radix.connect('http://www.test.com')
const loadKeystore = (): Promise<KeystoreT> =>
Promise.resolve(keystoreForTest.keystore)
type SystemUnderTest = {
plaintext: string
pkOfActiveSigningKey0: PublicKeyT
pkOfActiveSigningKey1: PublicKeyT
recipient: PublicKeyT
tx: SimpleExecutedTransaction
decrypted0: string
decrypted1: string
}
const recipientPK = PublicKey.fromBuffer(
Buffer.from(keystoreForTest.publicKeysCompressed[1], 'hex'),
)._unsafeUnwrap()
const recipientAddress = AccountAddress.fromPublicKeyAndNetwork({
publicKey: recipientPK,
network: Network.MAINNET,
})
const tokenTransferInput: TransferTokensInput = {
to: recipientAddress,
amount: 1,
tokenIdentifier: 'xrd_tr1qyf0x76s',
}
const plaintext = 'Hey Bob, this is Alice.'
let sut: SystemUnderTest = ({
plaintext,
recipient: recipientPK,
} as unknown) as SystemUnderTest
const txIntentBuilder = TransactionIntentBuilder.create()
// @ts-ignore
subs.add(
radix.__wallet
.pipe(
map(
(im: WalletT): AccountT => {
const account = im.__unsafeGetAccount()
sut.pkOfActiveSigningKey0 = account.publicKey
return account
},
),
mergeMap(
(account: AccountT): Observable<TransactionIntent> => {
return txIntentBuilder
.transferTokens(tokenTransferInput)
.message({ plaintext, encrypt: true })
.build({
encryptMessageIfAnyWithAccount: of(account),
})
},
),
map(
(
intent: TransactionIntent,
): SimpleExecutedTransaction =>
mockTransformIntentToExecutedTX(intent),
),
tap(
(
tx: SimpleExecutedTransaction,
): SimpleExecutedTransaction => {
sut.tx = tx
return tx
},
),
mergeMap(tx => radix.decryptTransaction(tx)),
tap((decrypted: string) => (sut.decrypted0 = decrypted)),
mergeMap(_ => {
radix.deriveNextAccount({ alsoSwitchTo: true })
return radix.activeAccount
}),
tap(
(account: AccountT) =>
(sut.pkOfActiveSigningKey1 = account.publicKey),
),
mergeMap(_ => radix.decryptTransaction(sut.tx)),
tap(decrypted => (sut.decrypted1 = decrypted)),
)
.subscribe({
next: _ => {
expect(sut).toBeDefined()
expect(sut.plaintext).toBeDefined()
expect(sut.decrypted0).toBeDefined()
expect(sut.decrypted1).toBeDefined()
expect(sut.pkOfActiveSigningKey0).toBeDefined()
expect(sut.pkOfActiveSigningKey1).toBeDefined()
expect(sut.recipient).toBeDefined()
expect(sut.tx).toBeDefined()
expect(sut.tx.message).toBeDefined()
expect(sut.tx.actions.length).toBe(1)
const transferTokensAction = sut.tx
.actions[0] as ExecutedTransferTokensAction
expect(
transferTokensAction.to.publicKey.equals(
sut.recipient,
),
).toBe(true)
expect(
sut.pkOfActiveSigningKey0.equals(
sut.pkOfActiveSigningKey1,
),
).toBe(false)
expect(
sut.pkOfActiveSigningKey1.equals(sut.recipient),
).toBe(true)
expect(sut.tx.message).not.toBe(sut.plaintext) // because encrypted
expect(sut.decrypted0).toBe(sut.plaintext)
expect(sut.decrypted1).toBe(sut.plaintext)
expect(sut.decrypted1).toBe(sut.decrypted0)
done()
},
error: error => {
const errMsg = msgFromError(error)
done(new Error(errMsg))
},
}),
)
radix.login(keystoreForTest.password, loadKeystore)
})
it('should be able to handle error on API call', done => {
const subs = new Subscription()
const errorMsg = 'failed to fetch native token'
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(
of({
...mockRadixCoreAPI(),
nativeToken: () => {
throw Error(errorMsg)
},
}),
)
subs.add(
radix.ledger.nativeToken().subscribe({
next: token => {
done(Error('Should throw'))
},
error: (e: APIError) => {
expect(e.message).toEqual(errorMsg)
done()
},
}),
)
})
describe('make tx single transfer', () => {
const tokenTransferInput: TransferTokensInput = {
to: bob,
amount: 1,
tokenIdentifier: 'xrd_tr1qyf0x76s',
}
let pollTXStatusTrigger: Observable<unknown>
const transferTokens = (): TransferTokensOptions => ({
transferInput: tokenTransferInput,
userConfirmation: 'skip',
pollTXStatusTrigger: pollTXStatusTrigger,
})
let subs: Subscription
beforeEach(() => {
subs = new Subscription()
pollTXStatusTrigger = interval(50)
})
afterEach(() => {
subs.unsubscribe()
})
it('events emits expected values', done => {
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(mockedAPI)
const expectedValues = [
TransactionTrackingEventType.INITIATED,
TransactionTrackingEventType.BUILT_FROM_INTENT,
TransactionTrackingEventType.ASKED_FOR_CONFIRMATION,
TransactionTrackingEventType.CONFIRMED,
TransactionTrackingEventType.SIGNED,
TransactionTrackingEventType.FINALIZED,
TransactionTrackingEventType.SUBMITTED,
TransactionTrackingEventType.UPDATE_OF_STATUS_OF_PENDING_TX,
TransactionTrackingEventType.UPDATE_OF_STATUS_OF_PENDING_TX,
TransactionTrackingEventType.COMPLETED,
]
subs.add(
radix
.transferTokens(transferTokens())
.events.pipe(
map(e => e.eventUpdateType),
take(expectedValues.length),
toArray(),
)
.subscribe({
next: values => {
expect(values).toStrictEqual(expectedValues)
done()
},
error: e => {
done(
new Error(
`Tx failed, even though we expected it to succeed, error: ${e.toString()}`,
),
)
},
}),
)
})
it('automatic confirmation', done => {
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(mockedAPI)
let gotTXId = false
subs.add(
radix.transferTokens(transferTokens()).completion.subscribe({
next: _txID => {
gotTXId = true
},
complete: () => {
done()
},
error: e => {
done(
new Error(
`Tx failed, but expected to succeed. Error ${e}`,
),
)
},
}),
)
})
it('radix_tx_manual_confirmation', done => {
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(mockedAPI)
const userConfirmation = new ReplaySubject<ManualUserConfirmTX>(1)
const transactionTracking = radix.transferTokens({
...transferTokens(),
userConfirmation,
})
let userHasBeenAskedToConfirmTX = false
subs.add(
userConfirmation.subscribe(confirmation => {
userHasBeenAskedToConfirmTX = true
confirmation.confirm()
}),
)
subs.add(
transactionTracking.completion.subscribe({
next: _txID => {
expect(userHasBeenAskedToConfirmTX).toBe(true)
done()
},
error: e => {
done(e)
},
}),
)
})
it('should not emit sign tx event when switching accounts', done => {
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(mockedAPI)
const userConfirmation = new ReplaySubject<ManualUserConfirmTX>()
let userConfirmationTriggerCount = 0
const transactionTracking = radix.transferTokens({
...transferTokens(),
userConfirmation,
})
let confirmTXFn: () => void = () => {
throw new Error('Confirm tx closure not set!')
}
subs.add(
userConfirmation.subscribe(n => {
userConfirmationTriggerCount += 1
confirmTXFn = n.confirm
}),
)
const confirmTxSubject = new Subject<undefined>()
subs.add(
confirmTxSubject.subscribe(_ => {
confirmTXFn()
}),
)
const accountSizeMin = 3
const accountSizeMax = accountSizeMin + 2
subs.add(
radix.accounts
.pipe(
map(acs => acs.size()),
skipWhile(
accountCount => accountCount < accountSizeMin,
),
filter(acsSize => acsSize < accountSizeMax),
)
.subscribe(acsSize => {
confirmTxSubject.next(undefined)
}),
)
radix.deriveNextAccount({ alsoSwitchTo: true })
radix.deriveNextAccount({ alsoSwitchTo: true })
subs.add(
transactionTracking.completion.subscribe({
next: _txID => {
expect(userConfirmationTriggerCount).toBe(1)
done()
},
error: e => {
done(e)
},
}),
)
radix.deriveNextAccount({ alsoSwitchTo: true })
})
it('should be able to call stake tokens', done => {
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(mockedAPI)
subs.add(
radix
.stakeTokens({
stakeInput: {
amount: 1,
validator:
'tv1qdqft0u899axwce955fkh9rundr5s2sgvhpp8wzfe3ty0rn0rgqj2x6y86p',
},
userConfirmation: 'skip',
pollTXStatusTrigger: pollTXStatusTrigger,
})
.completion.subscribe({
complete: () => {
done()
},
error: e => {
done(
new Error(
`Tx failed, but expected to succeed. Error ${e}`,
),
)
},
}),
)
})
it('should be able to call unstake tokens', done => {
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(mockedAPI)
subs.add(
radix
.unstakeTokens({
unstakeInput: {
amount: 1,
validator:
'tv1qdqft0u899axwce955fkh9rundr5s2sgvhpp8wzfe3ty0rn0rgqj2x6y86p',
},
userConfirmation: 'skip',
pollTXStatusTrigger: pollTXStatusTrigger,
})
.completion.subscribe({
complete: () => {
done()
},
error: e => {
done(
new Error(
`Tx failed, but expected to succeed. Error ${e}`,
),
)
},
}),
)
})
describe('transaction flow errors', () => {
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {})
})
afterAll(() => {
jest.clearAllMocks()
})
const testFailure = (
method: string,
cause: APIErrorCause,
done: any,
) => {
const errorMsg = mockErrorMsg(`TXFlow`)
const radix = Radix.create()
.__withWallet(createWallet())
.__withAPI(
of({
...mockRadixCoreAPI(),
[method]: (_intent: any) => {
throw Error(errorMsg)
},
}),
)
.logLevel(LogLevel.SILENT)
const transactionTracking = radix.transferTokens(
transferTokens(),
)
subs.add(
transactionTracking.completion.subscribe({
complete: () => {
done(
new Error(
'TX was successful, but we expected an error.',
),
)
},
error: (error: APIError) => {
expect(error.message).toBe(errorMsg)
expect(error.category).toEqual(ErrorCategory.API)
expect(error.cause).toEqual(cause)
done()
},
}),
)
}
it('buildTransaction', done => {
testFailure(
'buildTransaction',
APIErrorCause.BUILD_TRANSACTION_FAILED,
done,
)
})
it('submitSignedTransaction', done => {
testFailure(
'submitSignedTransaction',
APIErrorCause.SUBMIT_SIGNED_TX_FAILED,
done,
)
})
it('finalizeTransaction', done => {
testFailure(
'finalizeTransaction',
APIErrorCause.FINALIZE_TX_FAILED,
done,
)
})
})
})
it.skip('special signingKeychain with preallocated funds', done => {
const subs = new Subscription()
const walletWithFunds = makeWalletWithFunds(Network.MAINNET)
const radix = Radix.create()
.__withAPI(
of({
...mockRadixCoreAPI(),
networkId: (): Observable<Network> => {
return of(Network.MAINNET).pipe(shareReplay(1))
},
}),
)
.__withWallet(walletWithFunds)
const expectedAddresses: string[] = [
'brx1qsp8n0nx0muaewav2ksx99wwsu9swq5mlndjmn3gm9vl9q2mzmup0xqmhf7fh',
'brx1qspvvprlj3q76ltdxpz5qm54cp7dshrh3e9cemeu5746czdet3cfaegp6s708',
'brx1qsp0jvy2qxf93scsfy6ylp0cn4fzndf3epzcxmuekzrqrugnhnsrd7gah8wq5',
'brx1qspwfy7m78qsmq8ntq0yjpynpv2qfnrvzwgqacr4s360499tarzv6yctz3ahh',
'brx1qspzlz77f5dqwgyn2k62wfg2t3gj36ytsj7accv6kl9634tfkfqwleqmpz874',
]
subs.add(
radix.activeAddress
.pipe(
map((a: AccountAddressT) => a.toString()),
take(expectedAddresses.length),
toArray(),
)
.subscribe(
values => {
expect(values).toStrictEqual(expectedAddresses)
done()
},
error => {
done(error)
},
),
)
})
describe('tx history returns type of tx', () => {
const testTXType = (
fromMe: boolean,
toMe: boolean,
expectedTxType: TransactionType,
done: jest.DoneCallback,
): void => {
const subs = new Subscription()
const xrdRRI = ResourceIdentifier.fromUnsafe(
'xrd_tr1qyf0x76s',
)._unsafeUnwrap()
const txID = TransactionIdentifier.create(
'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef',
)._unsafeUnwrap()
const makeTransfer = (
input: Readonly<{
from: AccountAddressT
to: AccountAddressT
amount?: number
}>,
): ExecutedTransferTokensAction => {
return {
...input,
type: ActionType.TOKEN_TRANSFER,
rri: xrdRRI,
amount: Amount.fromUnsafe(
input.amount ?? 1337,
)._unsafeUnwrap(),
}
}
const wallet = makeWalletWithFunds(Network.MAINNET)
const network = Network.MAINNET
const myAddress = AccountAddress.fromPublicKeyAndNetwork({
publicKey: wallet.__unsafeGetAccount().publicKey,
network,
})
const mockApi = (): RadixCoreAPI => {
const makeTX = (): SimpleExecutedTransaction => {
return {
txID,
sentAt: new Date(),
fee: Amount.fromUnsafe(1)._unsafeUnwrap(),
actions: <ExecutedAction[]>[
makeTransfer({
from: fromMe ? myAddress : bob,
to: toMe ? myAddress : carol,
}),
{
type: ActionType.OTHER,
},
{
type: ActionType.STAKE_TOKENS,
from: fromMe ? myAddress : bob,
validator: ValidatorAddress.fromUnsafe(
'tv1qdqft0u899axwce955fkh9rundr5s2sgvhpp8wzfe3ty0rn0rgqj2x6y86p',
)._unsafeUnwrap(),
amount: Amount.fromUnsafe(1)._unsafeUnwrap(),
},
{
type: ActionType.UNSTAKE_TOKENS,
from: fromMe ? myAddress : bob,
validator: ValidatorAddress.fromUnsafe(
'tv1qdqft0u899axwce955fkh9rundr5s2sgvhpp8wzfe3ty0rn0rgqj2x6y86p',
)._unsafeUnwrap(),
amount: Amount.fromUnsafe(1)._unsafeUnwrap(),
},
],
}
}
return {
...mockRadixCoreAPI(),
lookupTransaction: _ => {
return of(makeTX())
},
transactionHistory: _ => {
return of({
cursor: 'AN_EMPTY_CURSOR',
transactions: <ExecutedTransaction[]>[makeTX()],
}).pipe(shareReplay(1))
},
}
}
const radix = Radix.create()
.__withWallet(wallet)
.__withAPI(of(mockApi()))
const assertTX = (tx: ExecutedTransaction): void => {
expect(tx.transactionType).toBe(expectedTxType)
}
subs.add(
radix.transactionHistory({ size: 1 }).subscribe(
hist => {
expect(hist.transactions.length).toBe(1)
const txFromhistory = hist.transactions[0]
assertTX(txFromhistory)
subs.add(
radix
.lookupTransaction(<TransactionIdentifierT>{})
.subscribe(
txFromLookup => {
assertTX(txFromLookup)
done()
},
error => {
new Error(
`Expected tx history but got error: ${msgFromError(
error,
)}`,
)
},
),
)
},
error => {
done(
new Error(
`Expected tx history but got error: ${msgFromError(
error,
)}`,
),
)
},
),
)
}
it('outgoing', done => {
testTXType(true, false, TransactionType.OUTGOING, done)
})
it('incoming', done => {
testTXType(false, true, TransactionType.INCOMING, done)
})
it('from_me_to_me', done => {
testTXType(true, true, TransactionType.FROM_ME_TO_ME, done)
})
it('unrelated', done => {
testTXType(false, false, TransactionType.UNRELATED, done)
})
})
})
*/