zumokit
Version:
ZumoKit is a Wallet as a Service SDK
825 lines (775 loc) • 25.5 kB
text/typescript
import { Decimal } from 'decimal.js';
import { errorProxy } from './utility';
import {
CurrencyCode,
Network,
AccountType,
AccountJSON,
AccountDataSnapshotJSON,
Address,
CardType,
CardStatus,
CardDetails,
AuthenticationConfig,
KbaAnswer,
CustodyType,
TradingPairJSON,
} from './interfaces';
import { Wallet } from './Wallet';
import { ZumoKitError } from './ZumoKitError';
import {
Account,
AccountFiatProperties,
AccountDataSnapshot,
Card,
ComposedTransaction,
ComposedExchange,
Transaction,
Exchange,
TradingPair,
} from './models';
/**
* User instance, obtained via {@link ZumoKit.signIn} method, provides methods for managing user wallet and accounts.
* <p>
* Refer to
* <a href="https://developers.zumo.money/docs/guides/manage-user-wallet">Manage User Wallet</a>,
* <a href="https://developers.zumo.money/docs/guides/create-fiat-account">Create Fiat Account</a>,
* <a href="https://developers.zumo.money/docs/guides/view-user-accounts">View User Accounts</a> and
* <a href="https://developers.zumo.money/docs/guides/get-account-data">Get Account Data</a>
* guides for usage details.
*/
export class User {
private zumoCoreModule: any;
private userImpl: any;
private accountDataListeners: Array<
(state: Array<AccountDataSnapshot>) => void
> = [];
private accountDataListenersImpl: Array<any> = [];
/** User identifier. */
id: string;
/** User integrator identifier. */
integratorId: string;
/** Indicator if user has wallet. */
hasWallet: boolean;
/** User accounts. */
accounts: Array<Account>;
/** @internal */
constructor(zumoCoreModule: any, userImpl: any) {
this.zumoCoreModule = zumoCoreModule;
this.userImpl = userImpl;
this.id = userImpl.getId();
this.integratorId = userImpl.getIntegratorId();
this.hasWallet = userImpl.hasWallet();
this.accounts = JSON.parse(userImpl.getAccounts()).map(
(json: AccountJSON) => new Account(json)
);
this.addAccountDataListener((snapshots) => {
this.accounts = snapshots.map((snapshot) => snapshot.account);
});
}
/**
* Create user wallet seeded by provided mnemonic and encrypted with user's password.
* <p>
* Mnemonic can be generated by {@link Utils.generateMnemonic} utility method.
* @param mnemonic mnemonic seed phrase
* @param password user provided password
*/
createWallet(mnemonic: string, password: string) {
const { zumoCoreModule } = this;
return errorProxy<Wallet>(zumoCoreModule, (resolve: any, reject: any) => {
this.userImpl.createWallet(
mnemonic,
password,
new zumoCoreModule.WalletCallbackWrapper({
onError: (error: string) => {
reject(new ZumoKitError(error));
},
onSuccess: (wallet: any) => {
this.hasWallet = true;
resolve(new Wallet(zumoCoreModule, wallet));
},
})
);
});
}
/**
* Recover user wallet with mnemonic seed phrase corresponding to user's wallet.
* This can be used if user forgets his password or wants to change his wallet password.
* @param mnemonic mnemonic seed phrase corresponding to user's wallet
* @param password user provided password
*/
recoverWallet(mnemonic: string, password: string) {
const { zumoCoreModule } = this;
return errorProxy<Wallet>(zumoCoreModule, (resolve: any, reject: any) => {
this.userImpl.recoverWallet(
mnemonic,
password,
new zumoCoreModule.WalletCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(wallet: any) {
resolve(new Wallet(zumoCoreModule, wallet));
},
})
);
});
}
/**
* Unlock user wallet with user's password.
* @param password user provided password
*/
unlockWallet(password: string) {
const { zumoCoreModule } = this;
return errorProxy<Wallet>(zumoCoreModule, (resolve: any, reject: any) => {
this.userImpl.unlockWallet(
password,
new zumoCoreModule.WalletCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(wallet: any) {
resolve(new Wallet(zumoCoreModule, wallet));
},
})
);
});
}
/**
* Reveal mnemonic seed phrase used to seed user wallet.
* @param password user provided password
*/
revealMnemonic(password: string) {
const { zumoCoreModule } = this;
return errorProxy<string>(zumoCoreModule, (resolve: any, reject: any) => {
this.userImpl.revealMnemonic(
password,
new zumoCoreModule.MnemonicCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(mnemonic: string) {
resolve(mnemonic);
},
})
);
});
}
/**
* Check if mnemonic seed phrase corresponds to user's wallet.
* This is useful for validating seed phrase before trying to recover wallet.
* @param mnemonic mnemonic seed phrase
*/
isRecoveryMnemonic(mnemonic: string): boolean {
try {
return this.userImpl.isRecoveryMnemonic(mnemonic);
} catch (exception) {
throw new ZumoKitError(this.zumoCoreModule.getException(exception));
}
}
/**
* Get account in specific currency, on specific network, with specific type.
* @param currencyCode currency code, e.g. 'BTC', 'ETH' or 'GBP'
* @param network network type, e.g. 'MAINNET', 'TESTNET' or 'RINKEBY'
* @param type account type, e.g. 'STANDARD', 'COMPATIBILITY' or 'SEGWIT'
* @param custodyType custody type, e.g. 'CUSTODY' or 'NON-CUSTODY'
*/
getAccount(
currencyCode: CurrencyCode,
network: Network,
type: AccountType,
custodyType: CustodyType
) {
const account = this.userImpl.getAccount(
currencyCode,
network,
type,
custodyType
);
if (account.hasValue()) {
return new Account(JSON.parse(account.get()));
}
return null;
}
/**
* Check if user is a registered fiat customer.
*/
isFiatCustomer(): boolean {
return this.userImpl.isFiatCustomer();
}
/**
* Make user fiat customer by providing user's personal details.
* @param firstName first name
* @param middleName middle name or null
* @param lastName last name
* @param dateOfBirth date of birth in ISO 8601 format, e.g '2020-08-12'
* @param email email
* @param phone phone number
* @param address home address
*/
makeFiatCustomer(
firstName: string,
middleName: string | null,
lastName: string,
dateOfBirth: string,
email: string,
phone: string,
address: Address
) {
return errorProxy<void>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
const optionalMiddleName = new this.zumoCoreModule.OptionalString();
if (middleName) optionalMiddleName.set(middleName);
this.userImpl.makeFiatCustomer(
firstName,
optionalMiddleName,
lastName,
dateOfBirth,
email,
phone,
JSON.stringify(address),
new this.zumoCoreModule.SuccessCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess() {
resolve();
},
})
);
}
);
}
/**
* Create custody or fiat account for specified currency. When creating a fiat account,
* user must already be fiat customer.
* @param currencyCode country code in ISO 4217 format, e.g. 'GBP'
*/
createAccount(currencyCode: CurrencyCode) {
return errorProxy<Account>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.createAccount(
currencyCode,
new this.zumoCoreModule.AccountCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(account: string) {
resolve(new Account(JSON.parse(account)));
},
})
);
}
);
}
/**
* Get nominated account details for specified account if it exists.
* Refer to
* <a href="https://developers.zumo.money/docs/guides/send-transactions#bitcoin">Create Fiat Account</a>
* for explanation about nominated account.
* @param accountId {@link Account Account} identifier
*/
getNominatedAccountFiatProperties(accountId: string) {
return errorProxy<AccountFiatProperties | null>(
this.zumoCoreModule,
(resolve: any) => {
this.userImpl.getNominatedAccountFiatProperties(
accountId,
new this.zumoCoreModule.AccountFiatPropertiesCallbackWrapper({
onError() {
resolve(null);
},
onSuccess(accountFiatProperties: string) {
resolve(
new AccountFiatProperties(JSON.parse(accountFiatProperties))
);
},
})
);
}
);
}
/**
* Fetch Strong Customer Authentication (SCA) config.
*/
fetchAuthenticationConfig() {
return errorProxy<void>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.fetchAuthenticationConfig(
new this.zumoCoreModule.AuthenticationConfigCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(authConfig: string) {
resolve(JSON.parse(authConfig) as AuthenticationConfig);
},
})
);
}
);
}
/**
* Create card for a fiat account.
* <p>
* At least one Knowledge-Based Authentication (KBA) answers should be defined,
* answers are limited to 256 characters and cannot be null or empty and only
* one answer per question type should be provided.
* @param fiatAccountId fiat {@link Account account} identifier
* @param cardType 'VIRTUAL' or 'PHYSICAL'
* @param mobileNumber card holder mobile number, starting with a '+', followed by the country code and then the mobile number, or null
* @param knowledgeBase list of KBA answers
*/
createCard(
fiatAccountId: string,
cardType: CardType,
mobileNumber: string,
knowledgeBase: Array<KbaAnswer>
) {
return errorProxy<void>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.createCard(
fiatAccountId,
cardType,
mobileNumber,
JSON.stringify(knowledgeBase),
new this.zumoCoreModule.CardCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(card: string) {
resolve(new Card(JSON.parse(card)));
},
})
);
}
);
}
/**
* Set card status to 'ACTIVE', 'BLOCKED' or 'CANCELLED'.
* - To block card, set card status to 'BLOCKED'.
* - To activate a physical card, set card status to 'ACTIVE' and provide PAN and CVC2 fields.
* - To cancel a card, set card status to 'CANCELLED'.
* - To unblock a card, set card status to 'ACTIVE.'.
* @param cardId {@link Card card} identifier
* @param cardStatus new card status
* @param pan PAN when activating a physical card, null otherwise (defaults to null)
* @param cvv2 CVV2 when activating a physical card, null otherwise (defaults to null)
*/
setCardStatus(
cardId: string,
cardStatus: CardStatus,
pan: string | null = null,
cvv2: string | null = null
) {
return errorProxy<void>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
const optionalPan = new this.zumoCoreModule.OptionalString();
if (pan) optionalPan.set(pan);
const optionalCvv2 = new this.zumoCoreModule.OptionalString();
if (cvv2) optionalCvv2.set(cvv2);
this.userImpl.setCardStatus(
cardId,
cardStatus,
optionalPan,
optionalCvv2,
new this.zumoCoreModule.SuccessCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess() {
resolve();
},
})
);
}
);
}
/**
* Reveals sensitive card details.
* @param cardId card identifier
*/
revealCardDetails(cardId: string) {
return errorProxy<void>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.revealCardDetails(
cardId,
new this.zumoCoreModule.CardDetailsCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(cardDetails: string) {
resolve(JSON.parse(cardDetails) as CardDetails);
},
})
);
}
);
}
/**
* Reveal card PIN.
* @param cardId {@link Card card} identifier
*/
revealPin(cardId: string) {
return errorProxy<void>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.revealPin(
cardId,
new this.zumoCoreModule.PinCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(pin: number) {
resolve(pin);
},
})
);
}
);
}
/**
* Unblock card PIN.
* @param cardId {@link Card card} identifier
*/
unblockPin(cardId: string) {
return errorProxy<void>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.unblockPin(
cardId,
new this.zumoCoreModule.SuccessCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess() {
resolve();
},
})
);
}
);
}
/**
* Add KBA answers to a card without SCA.
* <p>
* This endpoint is used to set Knowledge-Based Authentication (KBA) answers to
* a card without Strong Customer Authentication (SCA). Once it is set SCA flag
* on corresponding card is set to true.
* <p>
* At least one answer should be defined, answers are limited to 256 characters and
* cannot be null or empty and only one answer per question type should be provided.
*
* @param cardId card id
* @param knowledgeBase list of KBA answers
*/
setAuthentication(cardId: string, knowledgeBase: Array<KbaAnswer>) {
return errorProxy<void>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.setAuthentication(
cardId,
JSON.stringify(knowledgeBase),
new this.zumoCoreModule.SuccessCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess() {
resolve();
},
})
);
}
);
}
/**
* Listen to all account data changes.
*
* @param listener interface to listen to user changes
*/
addAccountDataListener(
listener: (snapshots: Array<AccountDataSnapshot>) => void
) {
const listenerImpl = new this.zumoCoreModule.AccountDataListenerWrapper({
onDataChange(snapshots: string) {
listener(
JSON.parse(snapshots).map(
(json: AccountDataSnapshotJSON) => new AccountDataSnapshot(json)
)
);
},
});
this.userImpl.addAccountDataListener(listenerImpl);
this.accountDataListeners.push(listener);
this.accountDataListenersImpl.push(listenerImpl);
}
/**
* Remove listener to state changes.
*
* @param listener interface to listen to state changes
*/
removeAccountDataListener(
listener: (snapshots: Array<AccountDataSnapshot>) => void
) {
let index;
// eslint-disable-next-line no-cond-assign
while ((index = this.accountDataListeners.indexOf(listener)) !== -1) {
this.accountDataListeners.splice(index, 1);
this.userImpl.removeAccountDataListener(
this.accountDataListenersImpl.splice(index, 1)[0]
);
}
}
/**
* Compose transaction between custody or fiat accounts in Zumo ecosystem.
* Refer to <a href="https://developers.zumo.money/docs/guides/send-transactions#internal-transaction">Send Transactions</a>
* guide for usage details.
*
* @param fromAccountId custody or fiat {@link Account Account} identifier
* @param toAccountId custody or fiat {@link Account Account} identifier
* @param amount amount in source account currency
* @param sendMax send maximum possible funds to destination (defaults to false)
*/
composeTransaction(
fromAccountId: string,
toAccountId: string,
amount: Decimal | null,
sendMax = false
) {
return errorProxy<ComposedTransaction>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
const amountOptional = new this.zumoCoreModule.OptionalDecimal();
if (amount)
amountOptional.set(
new this.zumoCoreModule.Decimal(amount.toString())
);
this.userImpl.composeTransaction(
fromAccountId,
toAccountId,
amountOptional,
sendMax,
new this.zumoCoreModule.ComposeTransactionCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(composedTransaction: string) {
resolve(new ComposedTransaction(JSON.parse(composedTransaction)));
},
})
);
}
);
}
/**
* Compose custody withdraw transaction from custody account.
* Refer to <a href="https://developers.zumo.money/docs/guides/send-transactions#custody-withdraw-transaction">Send Transactions</a>
* guide for usage details.
*
* @param fromAccountId custody or fiat {@link Account Account} identifier
* @param destination destination address or non-custodial account identifier
* @param amount amount in source account currency
* @param sendMax send maximum possible funds to destination (defaults to false)
*/
composeCustodyWithdrawTransaction(
fromAccountId: string,
destination: string,
amount: Decimal | null,
sendMax = false
) {
return errorProxy<ComposedTransaction>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
const amountOptional = new this.zumoCoreModule.OptionalDecimal();
if (amount)
amountOptional.set(
new this.zumoCoreModule.Decimal(amount.toString())
);
this.userImpl.composeCustodyWithdrawTransaction(
fromAccountId,
destination,
amountOptional,
sendMax,
new this.zumoCoreModule.ComposeTransactionCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(composedTransaction: string) {
resolve(new ComposedTransaction(JSON.parse(composedTransaction)));
},
})
);
}
);
}
/**
* Compose transaction from user fiat account to user's nominated account.
* Refer to <a href="https://developers.zumo.money/docs/guides/send-transactions#nominated-transaction">Send Transactions</a>
* guide for usage details.
*
* @param fromAccountId {@link Account Account} identifier
* @param amount amount in source account currency
* @param sendMax send maximum possible funds to destination (defaults to false)
*/
composeNominatedTransaction(
fromAccountId: string,
amount: Decimal | null,
sendMax = false
) {
return errorProxy<ComposedTransaction>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
const amountOptional = new this.zumoCoreModule.OptionalDecimal();
if (amount)
amountOptional.set(
new this.zumoCoreModule.Decimal(amount.toString())
);
this.userImpl.composeNominatedTransaction(
fromAccountId,
amountOptional,
sendMax,
new this.zumoCoreModule.ComposeTransactionCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(composedTransaction: string) {
resolve(new ComposedTransaction(JSON.parse(composedTransaction)));
},
})
);
}
);
}
/**
* Submit a transaction asynchronously.
* Refer to <a href="https://developers.zumo.money/docs/guides/send-transactions#submit-transaction">Send Transactions</a>
* guide for usage details.
*
* @param composedTransaction Composed transaction retrieved as a result
* of one of the compose transaction methods
* @param toAccountId Debit account id override, only applicable to direct custody deposits.
* In case no account id is specified senders custody account will be debited.
* @param metadata Optional metadata that will be attached to transaction
*/
submitTransaction(
composedTransaction: ComposedTransaction,
toAccountId: string | null = null,
metadata: any = null
) {
const optionalToAccountId = new this.zumoCoreModule.OptionalString();
if (toAccountId) optionalToAccountId.set(toAccountId);
return errorProxy<Transaction>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.submitTransaction(
JSON.stringify(composedTransaction.json),
optionalToAccountId,
JSON.stringify(metadata),
new this.zumoCoreModule.SubmitTransactionCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(transaction: string) {
resolve(new Transaction(JSON.parse(transaction)));
},
})
);
}
);
}
/**
* Fetch trading pairs that are currently supported.
*/
fetchTradingPairs() {
return errorProxy<void>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.fetchTradingPairs(
new this.zumoCoreModule.StringifiedJsonCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(stringifiedJSON: string) {
const tradingPairsJSON = JSON.parse(
stringifiedJSON
) as TradingPairJSON[];
resolve(tradingPairsJSON.map((json) => new TradingPair(json)));
},
})
);
}
);
}
/**
* Compose exchange asynchronously.
* Refer to <a href="https://developers.zumo.money/docs/guides/make-exchanges#compose-exchange">Make Exchanges</a>
* guide for usage details.
*
* @param debitAccountId {@link Account Account} identifier
* @param creditAccountId {@link Account Account} identifier
* @param debitAmount amount to be debited from debit account
* @param sendMax exchange maximum possible funds (defaults to false)
*/
composeExchange(
debitAccountId: string,
creditAccountId: string,
debitAmount: Decimal | null,
sendMax = false
) {
return errorProxy<ComposedExchange>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
const amountOptional = new this.zumoCoreModule.OptionalDecimal();
if (debitAmount)
amountOptional.set(
new this.zumoCoreModule.Decimal(debitAmount.toString())
);
this.userImpl.composeExchange(
debitAccountId,
creditAccountId,
amountOptional,
sendMax,
new this.zumoCoreModule.ComposeExchangeCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(composedExchange: string) {
resolve(new ComposedExchange(JSON.parse(composedExchange)));
},
})
);
}
);
}
/**
* Submit an exchange asynchronously.
* Refer to <a href="https://developers.zumo.money/docs/guides/make-exchanges#submit-exchange">Make Exchanges</a>
* guide for usage details.
*
* @param composedExchange Composed exchange retrieved as the result
* of {@link composeExchange} method
*/
submitExchange(composedExchange: ComposedExchange) {
return errorProxy<Exchange>(
this.zumoCoreModule,
(resolve: any, reject: any) => {
this.userImpl.submitExchange(
JSON.stringify(composedExchange.json),
new this.zumoCoreModule.SubmitExchangeCallbackWrapper({
onError(error: string) {
reject(new ZumoKitError(error));
},
onSuccess(exchange: string) {
resolve(new Exchange(JSON.parse(exchange)));
},
})
);
}
);
}
}