Skip to content

Commit

Permalink
feat(error): introduce error types (#1345)
Browse files Browse the repository at this point in the history
* feat(error): introduce error types

* feat(error): introduce error class in channels

* feat(error): introduce error class in tx

* chore: fix typos in the code

* feat(error): introduce error classes in utils

* feat(error): introduce error types in aepp-wallet communication

* chore: add missing error classes

* docs: error handling guide using classes
  • Loading branch information
subhod-i committed Jan 24, 2022
1 parent 6b71402 commit 444bb33
Show file tree
Hide file tree
Showing 55 changed files with 1,339 additions and 240 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -26,6 +26,7 @@ Usage guides:
- [æternity naming system](docs/guides/aens.md)
- [Contracts](docs/guides/contracts.md)
- [Contract events](docs/guides/contract-events.md)
- [Error handling](docs/guides/error-handling.md)
- [Oracles](docs/guides/oracles.md)
- [Low vs High level API](docs/guides/low-vs-high-usage.md)

Expand Down
172 changes: 172 additions & 0 deletions docs/guides/error-handling.md
@@ -0,0 +1,172 @@
# Error Handling

This guide shows you how to handle errors originating from the SDK. SDK by default exports the following error classes from file [errors.ts](../../src/utils/errors.ts)

## Error Hierarchy

```
AeError
│ IllegalArgumentError
│ IllegalArgumentError
│ InsufficientBalanceError
│ InvalidDenominationError
│ InvalidHashError
│ InvalidNameError
│ MissingParamError
│ NoBrowserFoundError
│ NoSerializerFoundError
│ RequestTimedOutError
│ TxTimedOutError
│ TypeError
│ UnknownHashClassError
│ UnsupportedPlatformError
│ UnsupportedProtocolError
└───AccountError
│ │ InvalidGaAddressError
│ │ InvalidKeypairError
│ │ UnavailableAccountError
└───AensError
│ │ AensNameNotFoundError
│ │ AensPointerContextError
│ │ InsufficientNameFeeError
│ │ InvalidAensNameError
└───AeppError
│ │ DuplicateCallbackError
│ │ InvalidRpcMessage
│ │ MissingCallbackError
│ │ UnAuthorizedAccountError
│ │ UnknownRpcClientError
│ │ UnsubscribedAccountError
└───ChannelError
│ │ ChannelCallError
│ │ ChannelConnectionError
│ │ ChannelInitializationError
│ │ ChannelPingTimedOutError
│ │ UnexpectedChannelMessageError
│ │ UnknownChannelStateError
└───CompilerError
│ │ UnavailableCompilerError
│ │ UnsupportedCompilerError
│ │ InvalidAuthDataError
└───ContractError
│ │ BytecodeMismatchError
│ │ DuplicateContractError
│ │ InactiveContractError
│ │ InvalidEventSchemaError
│ │ InvalidMethodInvocationError
│ │ MissingContractAddressError
│ │ MissingContractDefError
│ │ MissingFunctionNameError
│ │ NodeInvocationError
│ │ NoSuchContractError
│ │ NoSuchContractFunctionError
│ │ NotPayableFunctionError
│ │ UnknownCallReturnTypeError
└───CryptographyError
│ │ InvalidChecksumError
│ │ InvalidDerivationPathError
│ │ InvalidKeyError
│ │ InvalidMnemonicError
│ │ InvalidPasswordError
│ │ MerkleTreeHashMismatchError
│ │ MessageLimitError
│ │ MissingNodeInTreeError
│ │ NoSuchAlgorithmError
│ │ NotHardenedSegmentError
│ │ UnknownNodeLengthError
│ │ UnknownPathNibbleError
│ │ UnsupportedChildIndexError
│ │ UnsupportedKdfError
└───NodeError
│ │ DisconnectedError
│ │ DuplicateNodeError
│ │ NodeNotFoundError
│ │ UnsupportedNodeError
└───SwaggerError
└───TransactionError
│ │ DecodeError
│ │ DryRunError
│ │ IllegalBidFeeError
│ │ InvalidSignatureError
│ │ InvalidTxError
│ │ InvalidTxParamsError
│ │ NoDefaultAensPointerError
│ │ PrefixMismatchError
│ │ PrefixNotFoundError
│ │ SchemaNotFoundError
│ │ TagNotFoundError
│ │ TxNotInChainError
│ │ UnknownTxError
│ │ UnsignedTxError
│ │ UnsupportedABIversionError
│ │ UnsupportedVMversionError
└̌───WalletError
│ │ AlreadyConnectedError
│ │ MessageDirectionError
│ │ NoWalletConnectedError
│ │ RpcConnectionError
```

## Usage

```js
// import required error classes
const {
Universal,
Node,
MemoryAccount,
Crypto,
InvalidTxParamsError,
InvalidAensNameError
} = require('@aeternity/aepp-sdk')

// setup
const NODE_URL = 'https://testnet.aeternity.io'
const PAYER_ACCOUNT_KEYPAIR = Crypto.generateKeyPair()
const NEW_USER_KEYPAIR = Crypto.generateKeyPair()

const payerAccount = MemoryAccount({ keypair: PAYER_ACCOUNT_KEYPAIR })
const newUserAccount = MemoryAccount({ keypair: NEW_USER_KEYPAIR })
const node = await Node({ url: NODE_URL })
const client = await Universal({
nodes: [{ name: 'testnet', instance: node }],
accounts: [payerAccount, newUserAccount]
})

// catch exceptions
try {
const spendTxResult = await client.spend(-1, await newUserAccount.address(), { onAccount: payerAccount})
} catch(err) {
if(err instanceof InvalidTxParamsError){
console.log(`Amount specified is not valid, ${err.message}`)
} else if(err instanceof InvalidAensNameError) {
console.log(`Address specified is not valid, ${err.message}`)
}
}

// using generic error classes
const {AensError, TransactionError, AeError } = require('@aeternity/aepp-sdk')

try {
const spendTxResult = await client.spend(1, "ak_2tv", { onAccount: payerAccount})
} catch(err) {
if(err instanceof AensError){
// address or AENS related errors
} else if(err instanceof TransactionError) {
// transaction errors
} else if(err instanceof AeError){
// match any errors from the SDK
}
}
```
15 changes: 8 additions & 7 deletions src/account/memory.js
Expand Up @@ -26,6 +26,7 @@ import AccountBase from './base'
import { sign, isAddressValid, isValidKeypair } from '../utils/crypto'
import { isHex } from '../utils/string'
import { decode } from '../tx/builder/helpers'
import { InvalidKeypairError, InvalidGaAddressError } from '../utils/errors'

const secrets = new WeakMap()

Expand All @@ -44,21 +45,21 @@ export default AccountBase.compose({
init ({ keypair, gaId }) {
this.isGa = !!gaId
if (gaId) {
if (!isAddressValid(gaId)) throw new Error('Invalid GA address')
if (!isAddressValid(gaId)) throw new InvalidGaAddressError()
secrets.set(this, { publicKey: gaId })
return
}

if (!keypair || typeof keypair !== 'object') throw new Error('KeyPair must be an object')
if (!keypair.secretKey || !keypair.publicKey) throw new Error('KeyPair must must have "secretKey", "publicKey" properties')
if (typeof keypair.publicKey !== 'string' || keypair.publicKey.indexOf('ak_') === -1) throw new Error('Public Key must be a base58c string with "ak_" prefix')
if (!keypair || typeof keypair !== 'object') throw new InvalidKeypairError('KeyPair must be an object')
if (!keypair.secretKey || !keypair.publicKey) throw new InvalidKeypairError('KeyPair must must have "secretKey", "publicKey" properties')
if (typeof keypair.publicKey !== 'string' || keypair.publicKey.indexOf('ak_') === -1) throw new InvalidKeypairError('Public Key must be a base58c string with "ak_" prefix')
if (
!Buffer.isBuffer(keypair.secretKey) &&
(typeof keypair.secretKey === 'string' && !isHex(keypair.secretKey))
) throw new Error('Secret key must be hex string or Buffer')
) throw new InvalidKeypairError('Secret key must be hex string or Buffer')

const pubBuffer = Buffer.from(decode(keypair.publicKey, 'ak'))
if (!isValidKeypair(Buffer.from(keypair.secretKey, 'hex'), pubBuffer)) throw new Error('Invalid Key Pair')
if (!isValidKeypair(Buffer.from(keypair.secretKey, 'hex'), pubBuffer)) throw new InvalidKeypairError('Invalid Key Pair')

secrets.set(this, {
secretKey: Buffer.isBuffer(keypair.secretKey) ? keypair.secretKey : Buffer.from(keypair.secretKey, 'hex'),
Expand All @@ -68,7 +69,7 @@ export default AccountBase.compose({
props: { isGa: false },
methods: {
sign (data) {
if (this.isGa) throw new Error('You are trying to sign data using GA account without keypair')
if (this.isGa) throw new InvalidKeypairError('You are trying to sign data using GA account without keypair')
return Promise.resolve(sign(data, secrets.get(this).secretKey))
},
address () {
Expand Down
16 changes: 12 additions & 4 deletions src/account/multiple.js
Expand Up @@ -25,6 +25,10 @@ import AsyncInit from '../utils/async-init'
import MemoryAccount from './memory'
import { decode } from '../tx/builder/helpers'
import AccountBase, { isAccountBase } from './base'
import {
UnavailableAccountError,
TypeError
} from '../utils/errors'

/**
* AccountMultiple stamp
Expand Down Expand Up @@ -125,7 +129,7 @@ export default AccountBase.compose(AsyncInit, {
*/
selectAccount (address) {
decode(address, 'ak')
if (!this.accounts[address]) throw new Error(`Account for ${address} not available`)
if (!this.accounts[address]) throw new UnavailableAccountError(address)
this.selectedAddress = address
},
/**
Expand All @@ -136,17 +140,21 @@ export default AccountBase.compose(AsyncInit, {
*/
_resolveAccount (account) {
if (account === null) {
throw new Error('No account or wallet configured')
throw new TypeError(
'Account should be an address (ak-prefixed string), ' +
'keypair, or instance of account base, got null instead')
} else {
switch (typeof account) {
case 'string':
decode(account, 'ak')
if (!this.accounts[account]) throw new Error(`Account for ${account} not available`)
if (!this.accounts[account]) throw new UnavailableAccountError(account)
return this.accounts[account]
case 'object':
return isAccountBase(account) ? account : MemoryAccount({ keypair: account })
default:
throw new Error(`Unknown account type: ${typeof account} (account: ${account})`)
throw new TypeError(
'Account should be an address (ak-prefixed string), ' +
`keypair, or instance of account base, got ${account} instead`)
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/ae/aens.js
Expand Up @@ -32,6 +32,10 @@ import {
} from '../tx/builder/helpers'
import Ae from './'
import { CLIENT_TTL, NAME_FEE, NAME_TTL } from '../tx/builder/schema'
import {
InsufficientNameFeeError,
IllegalArgumentError
} from '../utils/errors'

/**
* Revoke a name
Expand Down Expand Up @@ -199,7 +203,7 @@ async function query (name, opt = {}) {
},
revoke: async (options = {}) => this.aensRevoke(name, { ...opt, ...options }),
extendTtl: async (nameTtl = NAME_TTL, options = {}) => {
if (!nameTtl || typeof nameTtl !== 'number' || nameTtl > NAME_TTL) throw new Error('Ttl must be an number and less then 180000 blocks')
if (!nameTtl || typeof nameTtl !== 'number' || nameTtl > NAME_TTL) throw new IllegalArgumentError('Ttl must be an number and less then 180000 blocks')

return {
...await this.aensUpdate(name, {}, { ...opt, ...options, nameTtl, extendPointers: true }),
Expand Down Expand Up @@ -238,7 +242,7 @@ async function claim (name, salt, options) {

const minNameFee = getMinimumNameFee(name)
if (opt.nameFee !== this.Ae.defaults.nameFee && minNameFee.gt(opt.nameFee)) {
throw new Error(`the provided fee ${opt.nameFee} is not enough to execute the claim, required: ${minNameFee}`)
throw new InsufficientNameFeeError(opt.nameFee, minNameFee)
}
opt.nameFee = opt.nameFee !== this.Ae.defaults.nameFee ? opt.nameFee : minNameFee
const claimTx = await this.nameClaimTx({
Expand Down
3 changes: 2 additions & 1 deletion src/ae/index.js
Expand Up @@ -29,6 +29,7 @@ import AccountBase from '../account/base'
import TxBuilder from '../tx/builder'
import BigNumber from 'bignumber.js'
import { AE_AMOUNT_FORMATS } from '../utils/amount-formatter'
import { IllegalArgumentError } from '../utils/errors'

/**
* Sign and post a transaction to the chain
Expand Down Expand Up @@ -94,7 +95,7 @@ async function spend (amount, recipientIdOrName, options) {
*/
async function transferFunds (fraction, recipientIdOrName, options) {
if (fraction < 0 || fraction > 1) {
throw new Error(`Fraction should be a number between 0 and 1, got ${fraction}`)
throw new IllegalArgumentError(`Fraction should be a number between 0 and 1, got ${fraction}`)
}
const opt = { ...this.Ae.defaults, ...options }
const recipientId = await this.resolveName(recipientIdOrName, 'account_pubkey', opt)
Expand Down
3 changes: 2 additions & 1 deletion src/ae/oracle.js
Expand Up @@ -32,6 +32,7 @@ import { pause } from '../utils/other'
import { oracleQueryId, decode } from '../tx/builder/helpers'
import { unpackTx } from '../tx/builder'
import { ORACLE_TTL, QUERY_FEE, QUERY_TTL, RESPONSE_TTL } from '../tx/builder/schema'
import { RequestTimedOutError } from '../utils/errors'

/**
* Constructor for Oracle Object (helper object for using Oracle)
Expand Down Expand Up @@ -129,7 +130,7 @@ export async function pollForQueryResponse (
return String(responseBuffer)
}
}
throw new Error(`Giving up after ${(attempts - 1) * interval}ms`)
throw new RequestTimedOutError((attempts - 1) * interval)
}

/**
Expand Down

0 comments on commit 444bb33

Please sign in to comment.