Skip to content

Commit

Permalink
Merge pull request #988 from lens-protocol/T-23102/wait-for-tx
Browse files Browse the repository at this point in the history
feat: SessionClient#waitForTransaction
  • Loading branch information
cesarenaldi authored Dec 3, 2024
2 parents 1e93e08 + 452c5aa commit e4c02d4
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 15 deletions.
68 changes: 53 additions & 15 deletions packages/client/src/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { Credentials, IStorage, IStorageProvider } from '@lens-protocol/sto
import { InMemoryStorageProvider, createCredentialsStorage } from '@lens-protocol/storage';
import {
ResultAsync,
type TxHash,
errAsync,
invariant,
never,
Expand All @@ -29,16 +30,20 @@ import {
import { type Logger, getLogger } from 'loglevel';

import { type AuthenticatedUser, authenticatedUser } from './AuthenticatedUser';
import { transactionStatus } from './actions';
import { configureContext } from './context';
import {
AuthenticationError,
GraphQLErrorCode,
SigningError,
TransactionIndexingError,
UnauthenticatedError,
UnexpectedError,
hasExtensionCode,
} from './errors';
import { decodeIdToken } from './tokens';
import type { StandardData } from './types';
import { delay } from './utils';

function takeValue<T>({
data,
Expand Down Expand Up @@ -101,30 +106,20 @@ export type LoginParams = ChallengeRequest & {
};

abstract class AbstractClient<TError> {
public readonly context: ClientContext;

protected readonly urql: UrqlClient;

protected readonly logger: Logger;

protected readonly credentials: IStorage<Credentials>;

protected constructor(options: ClientOptions) {
this.context = {
environment: options.environment,
cache: options.cache ?? false,
debug: options.debug ?? false,
origin: options.origin,
storage: options.storage ?? new InMemoryStorageProvider(),
};

this.credentials = createCredentialsStorage(this.context.storage, options.environment.name);
protected constructor(public readonly context: ClientContext) {
this.credentials = createCredentialsStorage(context.storage, context.environment.name);

this.logger = getLogger(this.constructor.name);
this.logger.setLevel(options.debug ? 'DEBUG' : 'SILENT');
this.logger.setLevel(context.debug ? 'DEBUG' : 'SILENT');

this.urql = createClient({
url: options.environment.backend,
url: context.environment.backend,
exchanges: [
mapExchange({
onOperation: async (operation: Operation) => {
Expand Down Expand Up @@ -217,7 +212,7 @@ export class PublicClient extends AbstractClient<UnexpectedError> {
* @returns The new instance of the client.
*/
static create(options: ClientOptions): PublicClient {
return new PublicClient(options);
return new PublicClient(configureContext(options));
}

/**
Expand Down Expand Up @@ -431,6 +426,49 @@ class SessionClient extends AbstractClient<UnauthenticatedError | UnexpectedErro
.map(takeValue);
}

/**
* Given a transaction hash, wait for the transaction to be either confirmed or rejected by the Lens API indexer.
*
* @param hash - The transaction hash to wait for.
* @returns The transaction hash if the transaction was confirmed or an error if the transaction was rejected.
*/
readonly waitForTransaction = (
txHash: TxHash,
): ResultAsync<TxHash, TransactionIndexingError | UnexpectedError> => {
return ResultAsync.fromPromise(this.pollTransactionStatus(txHash), (err) => {
if (err instanceof TransactionIndexingError || err instanceof UnexpectedError) {
return err;
}
return UnexpectedError.from(err);
});
};

protected async pollTransactionStatus(txHash: TxHash): Promise<TxHash> {
const startedAt = Date.now();

while (Date.now() - startedAt < this.context.environment.indexingTimeout) {
const result = await transactionStatus(this, { txHash });

if (result.isErr()) {
throw UnexpectedError.from(result.error);
}

switch (result.value.__typename) {
case 'FinishedTransactionStatus':
return txHash;

case 'FailedTransactionStatus':
throw TransactionIndexingError.from(result.value.reason);

case 'PendingTransactionStatus':
case 'NotIndexedYetStatus':
await delay(this.context.environment.pollingInterval);
break;
}
}
throw TransactionIndexingError.from(`Timeout waiting for transaction ${txHash}`);
}

protected override async fetchOptions(): Promise<RequestInit> {
const base = await super.fetchOptions();
const credentials = (await this.credentials.get()) ?? never('No credentials found');
Expand Down
37 changes: 37 additions & 0 deletions packages/client/src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { EnvironmentConfig } from '@lens-protocol/env';
import type { IStorageProvider } from '@lens-protocol/storage';

/**
* The client
*/
export type ClientConfig = {
/**
* The environment configuration to use (e.g. `mainnet`, `testnet`).
*/
environment: EnvironmentConfig;
/**
* Whether to enable caching.
*
* @defaultValue `false`
*/
cache?: boolean;
/**
* Whether to enable debug mode.
*
* @defaultValue `false`
*/
debug?: boolean;
/**
* The URL origin of the client.
*
* Use this to set the `Origin` header for requests from non-browser environments.
*/
origin?: string;

/**
* The storage provider to use.
*
* @defaultValue {@link InMemoryStorageProvider}
*/
storage?: IStorageProvider;
};
27 changes: 27 additions & 0 deletions packages/client/src/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import type { EnvironmentConfig } from '@lens-protocol/env';
import { type IStorageProvider, InMemoryStorageProvider } from '@lens-protocol/storage';
import type { ClientConfig } from './config';

/**
* @internal
*/
export type Context = {
environment: EnvironmentConfig;
cache: boolean;
debug: boolean;
origin?: string;
storage: IStorageProvider;
};

/**
* @internal
*/
export function configureContext(from: ClientConfig): Context {
return {
environment: from.environment,
cache: from.cache ?? false,
debug: from.debug ?? false,
origin: from.origin,
storage: from.storage ?? new InMemoryStorageProvider(),
};
}
14 changes: 14 additions & 0 deletions packages/client/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,20 @@ export class SigningError extends ResultAwareError {
name = 'SigningError' as const;
}

/**
* Error indicating a transaction failed.
*/
export class TransactionError extends ResultAwareError {
name = 'TransactionError' as const;
}

/**
* Error indicating a transaction failed to index.
*/
export class TransactionIndexingError extends ResultAwareError {
name = 'TransactionIndexingError' as const;
}

/**
* Error indicating an operation was not executed due to a validation error.
* See the `cause` property for more information.
Expand Down
1 change: 1 addition & 0 deletions packages/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ export type { IStorageProvider, InMemoryStorageProvider } from '@lens-protocol/s
export * from '@lens-protocol/types';

export * from './clients';
export * from './config';
export * from './errors';
6 changes: 6 additions & 0 deletions packages/client/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* @internal
*/
export function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
45 changes: 45 additions & 0 deletions packages/client/src/viem/viem.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { testnet } from '@lens-protocol/env';
import { describe, expect, it } from 'vitest';

import { chains } from '@lens-network/sdk/viem';
import { evmAddress, uri } from '@lens-protocol/types';
import { http, createWalletClient } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { handleWith } from '.';
import { post } from '../actions/post';
import { PublicClient } from '../clients';

const walletClient = createWalletClient({
account: privateKeyToAccount(import.meta.env.PRIVATE_KEY),
chain: chains.testnet,
transport: http(),
});

const owner = evmAddress(walletClient.account.address);
const app = evmAddress(import.meta.env.TEST_APP);
const account = evmAddress(import.meta.env.TEST_ACCOUNT);

const publicClient = PublicClient.create({
environment: testnet,
origin: 'http://example.com',
});

describe('Given an integration with viem', () => {
describe('When handling transaction actions', () => {
it('Then it should be possible to chain them with other helpers', async () => {
const authenticated = await publicClient.login({
accountOwner: { account, app, owner },
signMessage: (message: string) => walletClient.signMessage({ message }),
});
const sessionClient = authenticated._unsafeUnwrap();

const result = await post(sessionClient, {
contentUri: uri('https://devnet.irys.xyz/3n3Ujg3jPBHX58MPPqYXBSQtPhTgrcTk4RedJgV1Ejhb'),
})
.andThen(handleWith(walletClient))
.andThen(sessionClient.waitForTransaction);

expect(result.isOk(), result.isErr() ? result.error.message : undefined).toBe(true);
});
});
});
10 changes: 10 additions & 0 deletions packages/env/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { url, type URL, never } from '@lens-protocol/types';
export type EnvironmentConfig = {
name: string;
backend: URL;
indexingTimeout: number;
pollingInterval: number;
};

/**
Expand All @@ -17,6 +19,8 @@ export const mainnet: EnvironmentConfig = new Proxy(
{
name: 'mainnet',
backend: url('https://example.com'),
indexingTimeout: 10000,
pollingInterval: 1000,
},
{
get: (_target, _prop) => {
Expand All @@ -33,6 +37,8 @@ export const mainnet: EnvironmentConfig = new Proxy(
export const testnet: EnvironmentConfig = {
name: 'testnet',
backend: url('https://api.testnet.lens.dev/graphql'),
indexingTimeout: 10000,
pollingInterval: 1000,
};

/**
Expand All @@ -41,6 +47,8 @@ export const testnet: EnvironmentConfig = {
export const staging: EnvironmentConfig = {
name: 'staging',
backend: url('https://api.staging.lens.dev/graphql'),
indexingTimeout: 20000,
pollingInterval: 2000,
};

/**
Expand All @@ -49,4 +57,6 @@ export const staging: EnvironmentConfig = {
export const local: EnvironmentConfig = {
name: 'local',
backend: url('http://localhost:3000/graphql'),
indexingTimeout: 5000,
pollingInterval: 500,
};

0 comments on commit e4c02d4

Please sign in to comment.