Skip to content

Commit

Permalink
fix: txBuilder resolves Handles in OutputBilder.build()
Browse files Browse the repository at this point in the history
  • Loading branch information
VanessaPC committed May 30, 2023
1 parent 52d4659 commit caf857d
Show file tree
Hide file tree
Showing 12 changed files with 169 additions and 21 deletions.
Expand Up @@ -80,7 +80,7 @@ export class KoraLabsHandleProvider implements HandleProvider {
if (error.request) {
throw new ProviderError(ProviderFailure.ConnectionFailure, error, error.code);
}
throw new ProviderError(ProviderFailure.Unhealthy, error, `Failed to resolve handles due to: ${error.message}`);
throw new ProviderError(ProviderFailure.NotFound, error, `Failed to resolve handles due to: ${error.message}`);
}
if (error instanceof ProviderError) throw error;
throw new ProviderError(ProviderFailure.Unknown, error, 'Failed to resolve handles');
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/Provider/providerFactory.ts
Expand Up @@ -29,7 +29,7 @@ export class ProviderFactory<T> {
*
* @param name The name of the concrete provider implementation.
* @param params The parameters to be passed to the concrete implementation constructor.
* @param logger The logger instance ot be used in the service.
* @param logger The logger instance to be used in the service.
* @returns The new provider.
* @throws if The give provider name is not registered, or the constructor parameters of
* the providers are either missing or invalid.
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e/.env.example
Expand Up @@ -28,6 +28,8 @@ UTXO_PROVIDER=http
UTXO_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/utxo"}'
STAKE_POOL_PROVIDER=http
STAKE_POOL_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/stake-pool"}'
HANDLE_PROVIDER=handleProvider
HANDLE_PROVIDER_PARAMS='{"serverUrl:"http://localhost:4000","policyId":""}'

# Required by test:ogmios, test:blockfrost
DB_SYNC_CONNECTION_STRING='postgresql://postgres:doNoUseThisSecret!@localhost:5435/cexplorer'
Expand Down
37 changes: 34 additions & 3 deletions packages/e2e/src/factories.ts
Expand Up @@ -17,6 +17,7 @@ import {
CML,
Cardano,
ChainHistoryProvider,
HandleProvider,
NetworkInfoProvider,
ProviderFactory,
RewardsProvider,
Expand All @@ -33,10 +34,8 @@ import {
util
} from '@cardano-sdk/key-management';
import { CardanoWalletFaucetProvider, FaucetProvider } from './FaucetProvider';
import { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger';
import { Logger } from 'ts-log';
import { OgmiosTxSubmitProvider } from '@cardano-sdk/ogmios';
import {
KoraLabsHandleProvider,
assetInfoHttpProvider,
chainHistoryHttpProvider,
networkInfoHttpProvider,
Expand All @@ -45,6 +44,9 @@ import {
txSubmitHttpProvider,
utxoHttpProvider
} from '@cardano-sdk/cardano-services-client';
import { LedgerKeyAgent } from '@cardano-sdk/hardware-ledger';
import { Logger } from 'ts-log';
import { OgmiosTxSubmitProvider } from '@cardano-sdk/ogmios';
import { createConnectionObject } from '@cardano-ogmios/client';
import { createStubStakePoolProvider } from '@cardano-sdk/util-dev';
import { filter, firstValueFrom } from 'rxjs';
Expand All @@ -59,6 +61,7 @@ const customHttpFetchAdapter = isNodeJs ? undefined : require('@vespaiach/axios-
const HTTP_PROVIDER = 'http';
const OGMIOS_PROVIDER = 'ogmios';
const STUB_PROVIDER = 'stub';
const HANDLE_PROVIDER = 'handleProvider';
const MISSING_URL_PARAM = 'Missing URL';

export const faucetProviderFactory = new ProviderFactory<FaucetProvider>();
Expand All @@ -73,6 +76,7 @@ export const utxoProviderFactory = new ProviderFactory<UtxoProvider>();
export const stakePoolProviderFactory = new ProviderFactory<StakePoolProvider>();
export const bip32Ed25519Factory = new ProviderFactory<Crypto.Bip32Ed25519>();
export const addressDiscoveryFactory = new ProviderFactory<AddressDiscovery>();
export const handleProviderFactory = new ProviderFactory<HandleProvider>();

// Address Discovery strategies

Expand Down Expand Up @@ -160,6 +164,21 @@ utxoProviderFactory.register(HTTP_PROVIDER, async (params: any, logger: Logger):
});
});

handleProviderFactory.register(HANDLE_PROVIDER, async (params: any): Promise<HandleProvider> => {
if (params.serverUrl === undefined) throw new Error(`${KoraLabsHandleProvider.name}: ${MISSING_URL_PARAM}`);

return new Promise<HandleProvider>(async (resolve) => {
resolve(
new KoraLabsHandleProvider({
adapter: customHttpFetchAdapter,
networkInfoProvider: params.networkInfoProvider,
policyId: params.policyId,
serverUrl: params.serverUrl
})
);
});
});

// Stake Pool providers
stakePoolProviderFactory.register(
STUB_PROVIDER,
Expand Down Expand Up @@ -315,6 +334,18 @@ export const getWallet = async (props: GetWalletProps) => {
env.CHAIN_HISTORY_PROVIDER_PARAMS,
logger
),
handleProvider: await handleProviderFactory.create(
env.HANDLE_PROVIDER,
{
...env.HANDLE_PROVIDER_PARAMS,
networkInfoProvider: await networkInfoProviderFactory.create(
env.NETWORK_INFO_PROVIDER,
env.NETWORK_INFO_PROVIDER_PARAMS,
logger
)
},
logger
),
networkInfoProvider: await networkInfoProviderFactory.create(
env.NETWORK_INFO_PROVIDER,
env.NETWORK_INFO_PROVIDER_PARAMS,
Expand Down
2 changes: 2 additions & 0 deletions packages/e2e/test/web-extension/.env.example
Expand Up @@ -16,6 +16,8 @@ UTXO_PROVIDER=http
UTXO_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/utxo"}'
STAKE_POOL_PROVIDER=stub
STAKE_POOL_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/stake-pool"}'
HANDLE_PROVIDER=handleProvider
HANDLE_PROVIDER_PARAMS='{"serverUrl:"http://localhost:4000","policyId":""}'

# Test Environment
NETWORK_ID=0
Expand Down
16 changes: 13 additions & 3 deletions packages/tx-construction/src/tx-builder/OutputBuilder.ts
Expand Up @@ -23,11 +23,11 @@ export interface OutputBuilderProps {
/** Logger */
logger: Logger;
/** Handle Provider for resolving addresses */
handleProvider?: HandleProvider;
handleProvider: HandleProvider;
}

/** Determines if the `PartialTxOut` arg has at least an address and coins. */
const isViableTxOut = (txOut: PartialTxOut): txOut is OutputBuilderTxOut => !!(txOut?.address && txOut?.value?.coins);
const isViableTxOut = (txOut: PartialTxOut): txOut is Cardano.TxOut => !!(txOut?.address && txOut?.value?.coins);

/**
* Transforms from `OutputValidation` type emitted by `OutputValidator`, to
Expand Down Expand Up @@ -56,11 +56,13 @@ export class TxOutputBuilder implements OutputBuilder {
#partialOutput: PartialTxOut;
#outputValidator: OutputBuilderValidator;
#logger: Logger;
#handleProvider: HandleProvider;

constructor({ outputValidator, txOut, logger }: OutputBuilderProps) {
constructor({ outputValidator, txOut, logger, handleProvider }: OutputBuilderProps) {
this.#partialOutput = { ...txOut };
this.#outputValidator = outputValidator;
this.#logger = logger;
this.#handleProvider = handleProvider;
}

/**
Expand Down Expand Up @@ -132,6 +134,14 @@ export class TxOutputBuilder implements OutputBuilder {
throw outputValidation;
}

if (this.#partialOutput.handle) {
const resolution = await this.#handleProvider.resolveHandles({ handles: [this.#partialOutput.handle] });

if (resolution[0] !== null) {
txOut.handle = resolution[0];
}
}

return txOut;
}
}
7 changes: 5 additions & 2 deletions packages/tx-construction/src/tx-builder/TxBuilder.ts
@@ -1,5 +1,5 @@
import * as Crypto from '@cardano-sdk/crypto';
import { Cardano, Handle } from '@cardano-sdk/core';
import { Cardano, HandleProvider, HandleResolution } from '@cardano-sdk/core';
import { Logger } from 'ts-log';
import {
OutputBuilder,
Expand Down Expand Up @@ -91,7 +91,8 @@ export class GenericTxBuilder implements TxBuilder {
#outputValidator: OutputBuilderValidator;
#delegateConfig: DelegateConfig;
#logger: Logger;
#handles: Handle[];
#handleProvider: HandleProvider;
#handles: HandleResolution[];

constructor(dependencies: TxBuilderDependencies) {
this.#outputValidator =
Expand All @@ -101,6 +102,7 @@ export class GenericTxBuilder implements TxBuilder {
});
this.#dependencies = dependencies;
this.#logger = dependencies.logger;
this.#handleProvider = dependencies.handleProvider;
this.#handles = [];
}

Expand Down Expand Up @@ -134,6 +136,7 @@ export class GenericTxBuilder implements TxBuilder {

buildOutput(txOut?: PartialTxOut): OutputBuilder {
return new TxOutputBuilder({
handleProvider: this.#handleProvider,
logger: contextLogger(this.#logger, 'outputBuilder'),
outputValidator: this.#outputValidator,
txOut
Expand Down
9 changes: 5 additions & 4 deletions packages/tx-construction/src/tx-builder/types.ts
@@ -1,4 +1,4 @@
import { Cardano, Handle } from '@cardano-sdk/core';
import { Cardano, Handle, HandleProvider, HandleResolution } from '@cardano-sdk/core';
import { CustomError } from 'ts-custom-error';

import { InputSelectionError, InputSelector, SelectionSkeleton } from '@cardano-sdk/input-selection';
Expand Down Expand Up @@ -54,7 +54,7 @@ export type TxBodyValidationError = TxOutValidationError | InputSelectionError |
* appear in the final TxOut type since it's extracted before then and passed
* as `ctx`.
*/
export type OutputBuilderTxOut = Cardano.TxOut & { handle?: Handle };
export type OutputBuilderTxOut = Cardano.TxOut & { handle?: HandleResolution };

/**
* Helps build transaction outputs from its constituent parts.
Expand Down Expand Up @@ -101,7 +101,7 @@ export interface TxContext {
auxiliaryData?: Cardano.AuxiliaryData;
witness?: InitializeTxWitness;
isValid?: boolean;
handles?: Handle[];
handles?: HandleResolution[];
}

export type TxInspection = Cardano.TxBodyWithHash &
Expand All @@ -112,7 +112,7 @@ export type TxInspection = Cardano.TxBodyWithHash &
export interface SignedTx {
tx: Cardano.Tx;
ctx: {
handles: Handle[];
handles: HandleResolution[];
};
}

Expand Down Expand Up @@ -217,6 +217,7 @@ export interface TxBuilderDependencies {
txBuilderProviders: TxBuilderProviders;
logger: Logger;
outputValidator?: OutputBuilderValidator;
handleProvider: HandleProvider;
}

export type FinalizeTxDependencies = Pick<TxBuilderDependencies, 'inputResolver' | 'keyAgent'>;
4 changes: 2 additions & 2 deletions packages/tx-construction/src/types.ts
@@ -1,5 +1,5 @@
import * as Crypto from '@cardano-sdk/crypto';
import { Cardano, Handle } from '@cardano-sdk/core';
import { Cardano, HandleResolution } from '@cardano-sdk/core';
import { SelectionSkeleton } from '@cardano-sdk/input-selection';
import { SignTransactionOptions, TransactionSigner } from '@cardano-sdk/key-management';

Expand Down Expand Up @@ -31,7 +31,7 @@ export interface InitializeTxProps {
auxiliaryData?: Cardano.AuxiliaryData;
witness?: InitializeTxWitness;
signingOptions?: SignTransactionOptions;
handles?: Handle[];
handles?: HandleResolution[];
}

export interface InitializeTxPropsValidationResult {
Expand Down
65 changes: 60 additions & 5 deletions packages/tx-construction/test/tx-builder/TxBuilder.test.ts
Expand Up @@ -10,7 +10,7 @@ import {
util
} from '@cardano-sdk/key-management';
import { AssetId, mockProviders as mocks, somePartialStakePools } from '@cardano-sdk/util-dev';
import { CML, Cardano, Handle } from '@cardano-sdk/core';
import { CML, Cardano, Handle, ProviderError, ProviderFailure } from '@cardano-sdk/core';
import {
GenericTxBuilder,
OutputBuilderValidator,
Expand All @@ -29,9 +29,23 @@ function assertObjectRefsAreDifferent(obj1: unknown, obj2: unknown): void {
expect(obj1).not.toBe(obj2);
}

const resolvedHandle = {
handle: 'alice',
hasDatum: false,
policyId: Cardano.PolicyId('b0d07d45fe9514f80213f4020e5a61241458be626841cde717cb38a7'),
resolvedAddresses: {
cardano: Cardano.PaymentAddress('addr_test1vr8nl4u0u6fmtfnawx2rxfz95dy7m46t6dhzdftp2uha87syeufdg')
},
resolvedAt: {
hash: Cardano.BlockId('7a48b034645f51743550bbaf81f8a14771e58856e031eb63844738ca8ad72298'),
slot: Cardano.Slot(100)
}
};

describe('GenericTxBuilder', () => {
let outputValidator: jest.Mocked<OutputBuilderValidator>;
let txBuilder: GenericTxBuilder;
let txBuilderWithHandleErrors: GenericTxBuilder;
let txBuilderProviders: jest.Mocked<TxBuilderProviders>;
let output: Cardano.TxOut;
let output2: Cardano.TxOut;
Expand Down Expand Up @@ -77,6 +91,29 @@ describe('GenericTxBuilder', () => {
validateOutput: jest.fn().mockResolvedValue({ coinMissing: 0n } as OutputValidation)
};
txBuilder = new GenericTxBuilder({
handleProvider: {
healthCheck: jest.fn(),
resolveHandles: async () => [resolvedHandle]
},
inputResolver,
keyAgent: util.createAsyncKeyAgent(keyAgent),
logger: dummyLogger,
outputValidator,
txBuilderProviders
});
txBuilderWithHandleErrors = new GenericTxBuilder({
handleProvider: {
healthCheck: jest.fn(),
resolveHandles: async () => {
const error = new Error('not found');

throw new ProviderError(
ProviderFailure.NotFound,
error,
`Failed to resolve handles due to: ${error.message}`
);
}
},
inputResolver,
keyAgent: util.createAsyncKeyAgent(keyAgent),
logger: dummyLogger,
Expand Down Expand Up @@ -231,7 +268,7 @@ describe('GenericTxBuilder', () => {
address = Cardano.PaymentAddress('addr_test1vr8nl4u0u6fmtfnawx2rxfz95dy7m46t6dhzdftp2uha87syeufdg');
datumHash = Crypto.Hash32ByteBase16('3e33018e8293d319ef5b3ac72366dd28006bd315b715f7e7cfcbd3004129b80d');
output1Coin = 10_000_000n;
handle = '$adaHandle';
handle = 'alice';
output2Base = mocks.utxo[0][1];

outputBuilder = txBuilder.buildOutput().address(address).coin(output1Coin) as TxOutputBuilder;
Expand Down Expand Up @@ -356,6 +393,23 @@ describe('GenericTxBuilder', () => {
it('legit output with valid with address and coin', async () => {
await expect(txBuilder.buildOutput().address(address).coin(output1Coin).build()).resolves.toBeTruthy();
});

it('resolves handle to address', async () => {
const outputBuilderTx = await txBuilder
.buildOutput()
.address(address)
.handle('alice')
.coin(output1Coin)
.build();

expect(outputBuilderTx.handle).toBe(resolvedHandle);
});

it('rejects with an error when handle resolution fails', async () => {
await expect(
txBuilderWithHandleErrors.buildOutput().address(address).handle('alice').coin(output1Coin).build()
).rejects.toThrowError(/NOT_FOUND/);
});
});

describe('can validate required output fields', () => {
Expand Down Expand Up @@ -498,15 +552,16 @@ describe('GenericTxBuilder', () => {
});

it('returns a context with used handles along with the signed transaction', async () => {
const handle = '$adaHandle';
const tx = txBuilder
.addOutput({
...mocks.utxo[0][1],
handle
handle: resolvedHandle
})
.build();
const { handles } = await tx.inspect();
expect(handles).toEqual([resolvedHandle]);
const { ctx } = await tx.sign();
expect(ctx.handles).toEqual([handle]);
expect(ctx.handles).toEqual([resolvedHandle]);
});

it('can build transactions that are not modified by subsequent builder changes', async () => {
Expand Down

0 comments on commit caf857d

Please sign in to comment.