Skip to content

Commit

Permalink
feat: add HandleProvider interface and handle support implementation …
Browse files Browse the repository at this point in the history
…to TxBuilder
  • Loading branch information
VanessaPC committed May 31, 2023
1 parent 7255fb2 commit f209095
Show file tree
Hide file tree
Showing 22 changed files with 332 additions and 26 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/continuous-integration-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,5 @@ jobs:
STAKE_POOL_PROVIDER: 'http'
STAKE_POOL_PROVIDER_PARAMS: '{"baseUrl":"http://localhost:4000/stake-pool"}'
STAKE_POOL_PROVIDER_URL: 'http://localhost:4000/stake-pool'
HANDLE_PROVIDER: 'kora-labs'
HANDLE_PROVIDER_PARAMS: '{"serverUrl":"http://localhost:4000","policyId":""}'
1 change: 1 addition & 0 deletions packages/core/src/Provider/HandleProvider/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './types';
33 changes: 33 additions & 0 deletions packages/core/src/Provider/HandleProvider/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Cardano, Point, Provider } from '../..';

export type Handle = string;

/**
* @param policyId a hex encoded policyID
* @param handle a personalized string to identify a user
* @param hasDatum a boolean to indicated whether it contains a datum
* @param resolvedAddresses the addresses resolved from the handle
* @param resolvedAt the point at which the Handle was resolved
*/
export interface HandleResolution {
policyId: Cardano.PolicyId;
handle: Handle;
hasDatum: boolean;
resolvedAddresses: {
cardano: Cardano.PaymentAddress;
};
resolvedAt: Point;
}

export interface ResolveHandlesArgs {
handles: Handle[];
}

/**
* @param handles array
* @returns
* @param HandleResolution or null
*/
export interface HandleProvider extends Provider {
resolveHandles(args: ResolveHandlesArgs): Promise<Array<HandleResolution | null>>;
}
1 change: 1 addition & 0 deletions packages/core/src/Provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './UtxoProvider';
export * from './ChainHistoryProvider';
export * from './providerFactory';
export * from './types';
export * from './HandleProvider';
2 changes: 1 addition & 1 deletion packages/core/src/Provider/providerFactory.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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=kora-labs
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
6 changes: 5 additions & 1 deletion packages/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,11 @@ 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=kora-labs
HANDLE_PROVIDER_PARAMS='{"serverUrl":"http://localhost:4000","policyId":""}'
```

> :information_source: Notice that KEY_MANAGEMENT_PARAMS _mnemonic_ property is empty, if you leave this empty on the **local network's** e2e tests a new set of random mnemonics will be generated for you, this is the recommended way of setting up e2e tests on this network.
> :information*source: Notice that KEY_MANAGEMENT_PARAMS \_mnemonic* property is empty, if you leave this empty on the **local network's** e2e tests a new set of random mnemonics will be generated for you, this is the recommended way of setting up e2e tests on this network.
Then to run the local network tests, run:

Expand Down Expand Up @@ -244,6 +246,8 @@ ASSET_PROVIDER=http
ASSET_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/asset"}'
CHAIN_HISTORY_PROVIDER=http
CHAIN_HISTORY_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/chain-history"}'
HANDLE_PROVIDER=kora-labs
HANDLE_PROVIDER_PARAMS='{"serverUrl":"http://localhost:4000","policyId":""}'
NETWORK_INFO_PROVIDER=http
NETWORK_INFO_PROVIDER_PARAMS='{"baseUrl":"http://localhost:4000/network-info"}'
REWARDS_PROVIDER=http
Expand Down
26 changes: 26 additions & 0 deletions packages/e2e/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface ProviderParams {
baseUrl: string;
}

export interface HandleProviderParams {
serverUrl: string;
}

const baseValidator = <T>(value: string, schema: Schema, ...dependencySchemas: Schema[]) => {
const parsed = JSON.parse(value) as T;
const v = new SchemaValidator();
Expand Down Expand Up @@ -71,6 +75,24 @@ const providerParams = makeValidator((value) => {
return validated;
});

const handleProviderParams = makeValidator((value) => {
const validated = baseValidator<HandleProviderParams>(value, {
properties: { policyId: { type: 'string' }, serverUrl: { type: 'string' } },
required: ['serverUrl'],
type: 'object'
});

try {
// eslint-disable-next-line no-new
new URL(validated.serverUrl);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (error: any) {
throw new Error(`serverUrl: ${error.message}`);
}

return validated;
});

/**
* Shared across all tests
*/
Expand All @@ -82,6 +104,8 @@ const validators = {
CHAIN_HISTORY_PROVIDER: str(),
CHAIN_HISTORY_PROVIDER_PARAMS: providerParams(),
DB_SYNC_CONNECTION_STRING: str({ default: undefined }),
HANDLE_PROVIDER: str(),
HANDLE_PROVIDER_PARAMS: handleProviderParams(),
KEY_MANAGEMENT_PARAMS: keyManagementParams(),
KEY_MANAGEMENT_PROVIDER: str(),
LOGGER_MIN_SEVERITY: str({ default: 'info' }),
Expand Down Expand Up @@ -141,6 +165,8 @@ export const walletVariables = [
'ASSET_PROVIDER_PARAMS',
'CHAIN_HISTORY_PROVIDER',
'CHAIN_HISTORY_PROVIDER_PARAMS',
'HANDLE_PROVIDER',
'HANDLE_PROVIDER_PARAMS',
'KEY_MANAGEMENT_PARAMS',
'KEY_MANAGEMENT_PROVIDER',
'LOGGER_MIN_SEVERITY',
Expand Down
33 changes: 30 additions & 3 deletions packages/e2e/src/factories.ts
Original file line number Diff line number Diff line change
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 = 'kora-labs';
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,17 @@ 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 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 +330,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
Original file line number Diff line number Diff line change
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
1 change: 1 addition & 0 deletions packages/tx-construction/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@cardano-sdk/input-selection": "workspace:~",
"@cardano-sdk/key-management": "workspace:~",
"@cardano-sdk/util": "workspace:~",
"@cardano-sdk/wallet": "workspace:~",
"lodash": "^4.17.21",
"npm": "^9.3.0",
"rxjs": "^7.4.0",
Expand Down
46 changes: 40 additions & 6 deletions packages/tx-construction/src/tx-builder/OutputBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { Cardano } from '@cardano-sdk/core';
import { Cardano, Handle, HandleProvider } from '@cardano-sdk/core';
import { Hash32ByteBase16 } from '@cardano-sdk/crypto';
import { Logger } from 'ts-log';

import {
HandleNotFoundError,
InvalidConfigurationError,
OutputBuilder,
OutputBuilderTxOut,
OutputValidationMinimumCoinError,
OutputValidationMissingRequiredError,
OutputValidationTokenBundleSizeError,
PartialTxOut
PartialTxOut,
TxOutputFailure
} from './types';
import { OutputValidation, OutputValidator } from '../output-validation';

Expand All @@ -21,10 +25,13 @@ export interface OutputBuilderProps {
txOut?: PartialTxOut;
/** Logger */
logger: Logger;
/** Handle Provider for resolving addresses */
handleProvider?: HandleProvider;
}

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

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

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

if (handleProvider) {
this.#handleProvider = handleProvider;
}
}

/**
Expand All @@ -69,7 +81,7 @@ export class TxOutputBuilder implements OutputBuilder {
* @throws OutputValidationMissingRequiredError {@link OutputValidationMissingRequiredError} if
* the mandatory fields 'address' or 'coins' are missing
*/
toTxOut(): Cardano.TxOut {
toTxOut(): OutputBuilderTxOut {
if (!isViableTxOut(this.#partialOutput)) {
throw new OutputValidationMissingRequiredError(this.#partialOutput);
}
Expand Down Expand Up @@ -116,14 +128,36 @@ export class TxOutputBuilder implements OutputBuilder {
return this;
}

async build(): Promise<Cardano.TxOut> {
handle(handle: Handle): OutputBuilder {
if (!this.#handleProvider) {
throw new InvalidConfigurationError(TxOutputFailure.MissingHandleProviderError);
}

this.#partialOutput = { ...this.#partialOutput, handle };
return this;
}

async build(): Promise<OutputBuilderTxOut> {
const txOut = this.toTxOut();

const outputValidation = toOutputValidationError(txOut, await this.#outputValidator.validateOutput(txOut));
if (outputValidation) {
throw outputValidation;
}

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

if (resolution[0] !== null) {
txOut.handle = resolution[0];
txOut.address = resolution[0].resolvedAddresses.cardano;
} else {
// Throw an error because the handle resolved to null so we don't have
// an address for the transaction.
throw new HandleNotFoundError(this.#partialOutput);
}
}

return txOut;
}
}
Loading

0 comments on commit f209095

Please sign in to comment.