Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
module.exports = {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json",
"tsconfigRootDir": __dirname
},
"extends": [
"@atixlabs/eslint-config/configurations/node",
"plugin:@typescript-eslint/recommended",
Expand Down Expand Up @@ -27,6 +31,8 @@ module.exports = {
"unicorn/no-null": 0,
"unicorn/no-array-reduce": 0,
"unicorn/prefer-node-protocol": 0,
"@typescript-eslint/no-floating-promises": ["error"],
'@typescript-eslint/no-var-requires': 0, // covered by unicorn/prefer-module
'@typescript-eslint/explicit-module-boundary-types': 0,
"@typescript-eslint/ban-types": 0,
'@typescript-eslint/no-non-null-assertion': 0,
Expand Down
66 changes: 54 additions & 12 deletions packages/blockfrost/src/blockfrostProvider.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,58 @@
import { CardanoProvider } from '@cardano-sdk/core';
import { BlockFrostAPI } from '@blockfrost/blockfrost-js';
/* eslint-disable @typescript-eslint/no-explicit-any */
import { CardanoProvider, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { BlockFrostAPI, Error as BlockfrostError } from '@blockfrost/blockfrost-js';
import { Options } from '@blockfrost/blockfrost-js/lib/types';
import { BlockfrostToOgmios } from './BlockfrostToOgmios';

const formatBlockfrostError = (error: unknown) => {
const blockfrostError = error as BlockfrostError;
if (typeof blockfrostError === 'string') {
throw new ProviderError(ProviderFailure.Unknown, error, blockfrostError);
}
if (typeof blockfrostError !== 'object') {
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (response type)');
}
const errorAsType1 = blockfrostError as {
status_code: number;
message: string;
error: string;
};
if (errorAsType1.status_code) {
return errorAsType1;
}
const errorAsType2 = blockfrostError as {
errno: number;
message: string;
code: string;
};
if (errorAsType2.code) {
const status_code = Number.parseInt(errorAsType2.code);
if (!status_code) {
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (status code)');
}
return {
status_code,
message: errorAsType1.message,
error: errorAsType2.errno.toString()
};
}
throw new ProviderError(ProviderFailure.Unknown, error, 'failed to parse error (response json)');
};

const toProviderError = (error: unknown) => {
const { status_code } = formatBlockfrostError(error);
if (status_code === 404) {
throw new ProviderError(ProviderFailure.NotFound);
}
throw new ProviderError(ProviderFailure.Unknown, error, `status_code: ${status_code}`);
};

/**
* Connect to the [Blockfrost service](https://docs.blockfrost.io/)
*
* @param {Options} options BlockFrostAPI options
* @returns {CardanoProvider} CardanoProvider
*/

export const blockfrostProvider = (options: Options): CardanoProvider => {
const blockfrost = new BlockFrostAPI(options);

Expand Down Expand Up @@ -62,13 +105,7 @@ export const blockfrostProvider = (options: Options): CardanoProvider => {
};

const submitTx: CardanoProvider['submitTx'] = async (signedTransaction) => {
try {
const hash = await blockfrost.txSubmit(signedTransaction.to_bytes());

return !!hash;
} catch {
return false;
}
await blockfrost.txSubmit(signedTransaction.to_bytes());
};

const utxoDelegationAndRewards: CardanoProvider['utxoDelegationAndRewards'] = async (addresses, stakeKeyHash) => {
Expand Down Expand Up @@ -116,7 +153,7 @@ export const blockfrostProvider = (options: Options): CardanoProvider => {
return BlockfrostToOgmios.currentWalletProtocolParameters(response.data);
};

return {
const providerFunctions = {
ledgerTip,
networkInfo,
stakePoolStats,
Expand All @@ -125,5 +162,10 @@ export const blockfrostProvider = (options: Options): CardanoProvider => {
queryTransactionsByAddresses,
queryTransactionsByHashes,
currentWalletProtocolParameters
};
} as any;

return Object.keys(providerFunctions).reduce((provider, key) => {
provider[key] = (...args: any[]) => providerFunctions[key](...args).catch(toProviderError);
return provider;
}, {} as any) as CardanoProvider;
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CardanoProvider } from '@cardano-sdk/core';
import { CardanoProvider, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import { gql, GraphQLClient } from 'graphql-request';
import { TransactionSubmitResponse } from '@cardano-graphql/client-ts';
import { Schema as Cardano } from '@cardano-ogmios/client';
Expand Down Expand Up @@ -224,9 +224,11 @@ export const cardanoGraphqlDbSyncProvider = (uri: string): CardanoProvider => {
transaction: Buffer.from(signedTransaction.to_bytes()).toString('hex')
});

return !!response.hash;
} catch {
return false;
if (!response.hash) {
throw new Error('No "hash" in graphql response');
}
} catch (error) {
throw new ProviderError(ProviderFailure.Unknown, error);
}
};

Expand Down
1 change: 0 additions & 1 deletion packages/cip2/test/jest.setup.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable unicorn/prefer-module */
/* eslint-disable @typescript-eslint/no-var-requires */
const { testTimeout } = require('../jest.config');
require('fast-check').configureGlobal({
interruptAfterTimeLimit: testTimeout * 0.7,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/Ogmios/cslToOgmios.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { TxIn } from '@cardano-ogmios/schema';
import { Asset } from '..';
import { CSL } from '../CSL';
import { OgmiosValue } from './util';
Expand Down Expand Up @@ -30,3 +31,8 @@ export const value = (cslValue: CSL.Value): OgmiosValue => {
}
return result;
};

export const txIn = (input: CSL.TransactionInput): TxIn => ({
txId: Buffer.from(input.transaction_id().to_bytes()).toString('hex'),
index: input.index()
});
12 changes: 12 additions & 0 deletions packages/core/src/Provider/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CustomError } from 'ts-custom-error';

export enum ProviderFailure {
NotFound = 'NOT_FOUND',
Unknown = 'UNKNOWN'
}

export class ProviderError extends CustomError {
constructor(public reason: ProviderFailure, public innerError?: unknown, public detail?: string) {
super(reason + (detail ? ` (${detail})` : ''));
}
}
1 change: 1 addition & 0 deletions packages/core/src/Provider/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './types';
export * from './errors';
2 changes: 1 addition & 1 deletion packages/core/src/Provider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export interface CardanoProvider {
networkInfo: () => Promise<NetworkInfo>;
stakePoolStats?: () => Promise<StakePoolStats>;
/** @param signedTransaction signed and serialized cbor */
submitTx: (tx: CSL.Transaction) => Promise<boolean>;
submitTx: (signedTransaction: CSL.Transaction) => Promise<void>;
utxoDelegationAndRewards: (
addresses: Cardano.Address[],
stakeKeyHash: Cardano.Hash16
Expand Down
11 changes: 9 additions & 2 deletions packages/core/test/Ogmios/cslToOgmios.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CardanoSerializationLib, loadCardanoSerializationLib } from '@cardano-sdk/core';
import { AssetId } from '@cardano-sdk/util-dev';
import { AssetId, CslTestUtil } from '@cardano-sdk/util-dev';
import { cslToOgmios, ogmiosToCsl } from '../../src/Ogmios';

describe('util', () => {
Expand All @@ -8,7 +8,7 @@ describe('util', () => {
csl = await loadCardanoSerializationLib();
});

describe('valueToValueQuantities', () => {
describe('value', () => {
it('coin only', () => {
const coins = 100_000n;
const value = csl.Value.new(csl.BigNum.from_str(coins.toString()));
Expand All @@ -25,4 +25,11 @@ describe('util', () => {
expect(quantities.assets).toEqual(assets);
});
});

it('txIn', () => {
const cslInput = CslTestUtil.createTxInput(csl);
const txIn = cslToOgmios.txIn(cslInput);
expect(typeof txIn.index).toBe('number');
expect(typeof txIn.txId).toBe('string');
});
});
1 change: 1 addition & 0 deletions packages/util-dev/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * as AssetId from './assetId';
export * as CslTestUtil from './cslTestUtil';
export * as SelectionConstraints from './selectionConstraints';
export * from './util';
2 changes: 2 additions & 0 deletions packages/util-dev/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const flushPromises = (setImmediate = global.setImmediate) =>
new Promise((resolve) => setImmediate(resolve, void 0));
1 change: 1 addition & 0 deletions packages/wallet/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
dist
coverage
29 changes: 5 additions & 24 deletions packages/wallet/README.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
# Cardano JS SDK | Wallet

# Examples
See [integration tests] for usage examples

## Delegation
## Tests

```typescript
import { loadCardanoSerializationLib } from '@cardano-sdk/core';
import { createSingleAddressWallet, KeyManagement, Transaction, SingleAddressWalletDependencies } from '@cardano-sdk/wallet';
See [code coverage report]

async () => {
const csl = await loadCardanoSerializationLib();
const keyManager = KeyManagement.createInMemoryKeyManager({ csl, ... });
const wallet = await createSingleAddressWallet({ name: 'some-wallet' }, { csl, keyManager, ... });

const certs = new Transaction.CertificateFactory(keyManager);
const { body, hash } = await wallet.initializeTx({
certificates: [certs.stakeKeyDeregistration()],
withdrawals: [Transaction.withdrawal(csl, keyManager, 5_000_000n)],
...
});

// Calculated fee is returned by invoking body.fee()

const tx = await wallet.signTx(body, hash);

await wallet.submitTx(tx);
}
```
[integration tests]: https://github.com/input-output-hk/cardano-js-sdk/tree/master/packages/wallet/test/integration
[code coverage report]: https://input-output-hk.github.io/cardano-js-sdk/coverage/wallet
8 changes: 5 additions & 3 deletions packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"build": "tsc --build ./src",
"tscNoEmit": "shx echo typescript --noEmit command not implemented yet",
"cleanup": "shx rm -rf dist node_modules",
"coverage": "shx echo No coverage report for this package",
"coverage": "yarn test --coverage",
"lint": "eslint --ignore-path ../../.eslintignore \"**/*.ts\"",
"test": "jest -c ./jest.config.js",
"test:debug": "DEBUG=true yarn test"
Expand All @@ -30,9 +30,11 @@
"@cardano-ogmios/schema": "4.1.0",
"@cardano-sdk/cip2": "0.1.3",
"@cardano-sdk/core": "0.1.3",
"lodash-es": "^4.17.21",
"isomorphic-bip39": "^3.0.5",
"buffer": "^6.0.3",
"delay": "^5.0.0",
"emittery": "^0.10.0",
"isomorphic-bip39": "^3.0.5",
"lodash-es": "^4.17.21",
"ts-custom-error": "^3.2.0",
"ts-log": "^2.2.3"
}
Expand Down
87 changes: 87 additions & 0 deletions packages/wallet/src/InMemoryTransactionTracker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { TransactionTracker, TransactionTrackerEvents } from './types';
import Emittery from 'emittery';
import { Hash16, Slot, Tip } from '@cardano-ogmios/schema';
import { CardanoProvider, ProviderError, CardanoSerializationLib, CSL, ProviderFailure } from '@cardano-sdk/core';
import { TransactionError, TransactionFailure } from './TransactionError';
import { dummyLogger, Logger } from 'ts-log';
import delay from 'delay';
import { TransactionTrackerEvent } from '.';

export type Milliseconds = number;

export interface InMemoryTransactionTrackerProps {
provider: CardanoProvider;
csl: CardanoSerializationLib;
logger?: Logger;
pollInterval?: Milliseconds;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's add a doc entry for this one, as it's not fully expressed in the name

}

export class InMemoryTransactionTracker extends Emittery<TransactionTrackerEvents> implements TransactionTracker {
readonly #provider: CardanoProvider;
readonly #pendingTransactions = new Map<string, Promise<void>>();
readonly #csl: CardanoSerializationLib;
readonly #logger: Logger;
readonly #pollInterval: number;

constructor({ provider, csl, logger = dummyLogger, pollInterval = 2000 }: InMemoryTransactionTrackerProps) {
super();
this.#provider = provider;
this.#csl = csl;
this.#logger = logger;
this.#pollInterval = pollInterval;
}

async track(transaction: CSL.Transaction, submitted: Promise<void> = Promise.resolve()): Promise<void> {
await submitted;
const body = transaction.body();
const hash = Buffer.from(this.#csl.hash_transaction(body).to_bytes()).toString('hex');
this.#logger.debug('InMemoryTransactionTracker.trackTransaction', hash);

if (this.#pendingTransactions.has(hash)) {
return this.#pendingTransactions.get(hash)!;
}

const invalidHereafter = body.ttl();
if (!invalidHereafter) {
throw new TransactionError(TransactionFailure.CannotTrack, undefined, 'no TTL');
}

const promise = this.#checkTransactionViaProvider(hash, invalidHereafter);
this.#pendingTransactions.set(hash, promise);
this.emit(TransactionTrackerEvent.NewTransaction, { transaction, confirmed: promise }).catch(this.#logger.error);
void promise.catch(() => void 0).then(() => this.#pendingTransactions.delete(hash));

return promise;
}

async #checkTransactionViaProvider(hash: Hash16, invalidHereafter: Slot): Promise<void> {
await delay(this.#pollInterval);
try {
const tx = await this.#provider.queryTransactionsByHashes([hash]);
if (tx.length > 0) return; // done
return this.#onTransactionNotFound(hash, invalidHereafter);
} catch (error: unknown) {
if (error instanceof ProviderError && error.reason === ProviderFailure.NotFound) {
return this.#onTransactionNotFound(hash, invalidHereafter);
}
throw new TransactionError(TransactionFailure.CannotTrack, error);
}
}

async #onTransactionNotFound(hash: string, invalidHereafter: number) {
let tip: Tip | undefined;
try {
tip = await this.#provider.ledgerTip();
} catch (error: unknown) {
throw new TransactionError(
TransactionFailure.CannotTrack,
error,
"can't query tip to check for transaction timeout"
);
}
if (tip && tip.slot > invalidHereafter) {
throw new TransactionError(TransactionFailure.Timeout);
}
return this.#checkTransactionViaProvider(hash, invalidHereafter);
}
}
Loading