Skip to content

Commit

Permalink
feat: adds self-funded support for unfollow (#775)
Browse files Browse the repository at this point in the history
* feat: adds self-funded support for unfollow

* chore: changeset

* docs: unfollow self-funded docs

* fix: tests
  • Loading branch information
reecejohnson authored Dec 13, 2023
1 parent 21c643d commit dd2f7d2
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 39 deletions.
8 changes: 8 additions & 0 deletions .changeset/cuddly-emus-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@lens-protocol/domain": minor
"@lens-protocol/react": minor
"@lens-protocol/react-native": minor
"@lens-protocol/react-web": minor
---

feat: adds self-funded support for unfollow
19 changes: 18 additions & 1 deletion packages/domain/src/use-cases/profile/UnfollowProfile.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { ProfileId, TransactionKind } from '../../entities';
import { DelegableSigning } from '../transactions/DelegableSigning';
import { PaidTransaction } from '../transactions/PaidTransaction';
import { SponsorshipReady } from '../transactions/SponsorshipReady';

export type UnfollowRequest = {
profileId: ProfileId;
kind: TransactionKind.UNFOLLOW_PROFILE;
signless: boolean;
sponsored: boolean;
};

export class UnfollowProfile extends DelegableSigning<UnfollowRequest> {}
export class UnfollowProfile extends SponsorshipReady<UnfollowRequest> {
constructor(
private readonly delegableExecution: DelegableSigning<UnfollowRequest>,
private readonly paidExecution: PaidTransaction<UnfollowRequest>,
) {
super();
}

protected override async charged(request: UnfollowRequest): Promise<void> {
await this.paidExecution.execute(request);
}
protected override async sponsored(request: UnfollowRequest): Promise<void> {
await this.delegableExecution.execute(request);
}
}
1 change: 1 addition & 0 deletions packages/domain/src/use-cases/profile/__helpers__/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export function mockUnfollowRequest(): UnfollowRequest {
return {
profileId: mockProfileId(),
kind: TransactionKind.UNFOLLOW_PROFILE,
sponsored: true,
signless: true,
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import {
CreateUnfollowTypedDataDocument,
CreateUnfollowBroadcastItemResult,
CreateUnfollowTypedDataData,
CreateUnfollowTypedDataDocument,
CreateUnfollowTypedDataVariables,
SafeApolloClient,
omitTypename,
RelaySuccess,
SafeApolloClient,
UnfollowData,
UnfollowVariables,
UnfollowDocument,
CreateUnfollowBroadcastItemResult,
UnfollowVariables,
omitTypename,
} from '@lens-protocol/api-bindings';
import { lensHub } from '@lens-protocol/blockchain-bindings';
import { NativeTransaction, Nonce } from '@lens-protocol/domain/entities';
Expand All @@ -22,17 +22,22 @@ import { ChainType, Data, PromiseResult, success } from '@lens-protocol/shared-k
import { v4 } from 'uuid';

import { UnsignedProtocolCall } from '../../../wallet/adapters/ConcreteWallet';
import { IProviderFactory } from '../../../wallet/adapters/IProviderFactory';
import { AbstractContractCallGateway, ContractCallDetails } from '../AbstractContractCallGateway';
import { ITransactionFactory } from '../ITransactionFactory';
import { SelfFundedProtocolTransactionRequest } from '../SelfFundedProtocolTransactionRequest';
import { handleRelayError } from '../relayer';

export class UnfollowProfileGateway
extends AbstractContractCallGateway<UnfollowRequest>
implements IDelegatedTransactionGateway<UnfollowRequest>, ISignedOnChainGateway<UnfollowRequest>
{
constructor(
providerFactory: IProviderFactory,
private readonly apolloClient: SafeApolloClient,
private readonly transactionFactory: ITransactionFactory<UnfollowRequest>,
) {}
) {
super(providerFactory);
}

async createDelegatedTransaction(
request: UnfollowRequest,
Expand Down Expand Up @@ -62,10 +67,14 @@ export class UnfollowProfileGateway
id: result.id,
request,
typedData: omitTypename(result.typedData),
fallback: this.createRequestFallback(request, result),
});
}

protected async createEncodedData(request: UnfollowRequest): Promise<ContractCallDetails> {
const result = await this.createTypedData(request);
return this.createUnfollowCallData(result);
}

private async relayWithProfileManager(
request: UnfollowRequest,
): PromiseResult<RelaySuccess, BroadcastingError> {
Expand All @@ -79,10 +88,7 @@ export class UnfollowProfileGateway
});

if (data.result.__typename === 'LensProfileManagerRelayError') {
const result = await this.createTypedData(request);
const fallback = this.createRequestFallback(request, result);

return handleRelayError(data.result, fallback);
return handleRelayError(data.result);
}

return success(data.result);
Expand All @@ -105,17 +111,13 @@ export class UnfollowProfileGateway
return data.result;
}

private createRequestFallback(
request: UnfollowRequest,
result: CreateUnfollowBroadcastItemResult,
): SelfFundedProtocolTransactionRequest<UnfollowRequest> {
private createUnfollowCallData(result: CreateUnfollowBroadcastItemResult): ContractCallDetails {
const contract = lensHub(result.typedData.domain.verifyingContract);
const encodedData = contract.interface.encodeFunctionData('unfollow', [
result.typedData.message.unfollowerProfileId,
result.typedData.message.idsOfProfilesToUnfollow,
]);
return {
...request,
contractAddress: result.typedData.domain.verifyingContract,
encodedData: encodedData as Data,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,72 @@
/*
* @jest-environment node
*/
import { SafeApolloClient } from '@lens-protocol/api-bindings';
import {
mockLensApolloClient,
mockRelaySuccessFragment,
mockCreateUnfollowTypedDataData,
mockCreateUnfollowTypedDataResponse,
mockLensApolloClient,
mockRelaySuccessFragment,
mockUnfollowResponse,
} from '@lens-protocol/api-bindings/mocks';
import { NativeTransaction } from '@lens-protocol/domain/entities';
import { mockUnfollowRequest } from '@lens-protocol/domain/mocks';
import { NativeTransaction, UnsignedTransaction } from '@lens-protocol/domain/entities';
import { mockUnfollowRequest, mockWallet } from '@lens-protocol/domain/mocks';
import { ChainType } from '@lens-protocol/shared-kernel';
import { providers } from 'ethers';
import { mock } from 'jest-mock-extended';

import { UnsignedProtocolCall } from '../../../../wallet/adapters/ConcreteWallet';
import { mockIProviderFactory } from '../../../../wallet/adapters/__helpers__/mocks';
import { UnsignedContractCallTransaction } from '../../AbstractContractCallGateway';
import { assertUnsignedProtocolCallCorrectness } from '../../__helpers__/assertions';
import { mockITransactionFactory } from '../../__helpers__/mocks';
import { mockITransactionFactory, mockJsonRpcProvider } from '../../__helpers__/mocks';
import { UnfollowProfileGateway } from '../UnfollowProfileGateway';

function setupTestScenario({ apolloClient }: { apolloClient: SafeApolloClient }) {
function setupTestScenario({
apolloClient,
provider = mock<providers.JsonRpcProvider>(),
}: {
apolloClient: SafeApolloClient;
provider?: providers.JsonRpcProvider;
}) {
const transactionFactory = mockITransactionFactory();
const providerFactory = mockIProviderFactory({
chainType: ChainType.POLYGON,
provider,
});

const gateway = new UnfollowProfileGateway(apolloClient, transactionFactory);
const gateway = new UnfollowProfileGateway(providerFactory, apolloClient, transactionFactory);

return { gateway };
}

describe(`Given an instance of ${UnfollowProfileGateway.name}`, () => {
const request = mockUnfollowRequest();

describe(`when creating an ${UnsignedTransaction.name}<UnfollowRequest>`, () => {
const wallet = mockWallet();
const data = mockCreateUnfollowTypedDataData();

it(`should succeed with the expected ${UnsignedContractCallTransaction.name}`, async () => {
const provider = await mockJsonRpcProvider();
const apolloClient = mockLensApolloClient([
mockCreateUnfollowTypedDataResponse({
variables: {
request: {
unfollow: [request.profileId],
},
},
data,
}),
]);
const { gateway } = setupTestScenario({ apolloClient, provider });

const unsignedTransaction = await gateway.createUnsignedTransaction(request, wallet);

expect(unsignedTransaction).toBeInstanceOf(UnsignedContractCallTransaction);
});
});

describe(`when creating an IUnsignedProtocolCall<UnfollowRequest>`, () => {
it(`should create an instance of the ${UnsignedProtocolCall.name} with the expected typed data`, async () => {
const data = mockCreateUnfollowTypedDataData();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const UnfollowRequestSchema = z.object({
profileId: ProfileIdSchema,
kind: z.literal(TransactionKind.UNFOLLOW_PROFILE),
signless: z.boolean(),
sponsored: z.boolean(),
});

export const UpdateProfileManagersRequestSchema = z
Expand Down
33 changes: 26 additions & 7 deletions packages/react/src/transactions/adapters/useUnfollowController.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import {
InsufficientGasError,
PendingSigningRequestError,
UserRejectedError,
WalletConnectionError,
} from '@lens-protocol/domain/entities';
import { UnfollowProfile, UnfollowRequest } from '@lens-protocol/domain/use-cases/profile';
import { BroadcastingError, SignedOnChain } from '@lens-protocol/domain/use-cases/transactions';
import {
BroadcastingError,
DelegableSigning,
PaidTransaction,
SignedOnChain,
} from '@lens-protocol/domain/use-cases/transactions';
import { PromiseResult } from '@lens-protocol/shared-kernel';

import { useSharedDependencies } from '../../shared';
Expand All @@ -21,23 +27,32 @@ export function useUnfollowController() {
transactionFactory,
transactionGateway,
transactionQueue,
providerFactory,
} = useSharedDependencies();

return async (
request: UnfollowRequest,
): PromiseResult<
AsyncTransactionResult<void>,
BroadcastingError | PendingSigningRequestError | UserRejectedError | WalletConnectionError
| BroadcastingError
| InsufficientGasError
| PendingSigningRequestError
| UserRejectedError
| WalletConnectionError
> => {
validateUnfollowRequest(request);

const presenter = new TransactionResultPresenter<
UnfollowRequest,
BroadcastingError | PendingSigningRequestError | UserRejectedError | WalletConnectionError
| BroadcastingError
| InsufficientGasError
| PendingSigningRequestError
| UserRejectedError
| WalletConnectionError
>();
const gateway = new UnfollowProfileGateway(apolloClient, transactionFactory);
const gateway = new UnfollowProfileGateway(providerFactory, apolloClient, transactionFactory);

const signedUnfollow = new SignedOnChain(
const signedExecution = new SignedOnChain(
activeWallet,
transactionGateway,
gateway,
Expand All @@ -46,13 +61,17 @@ export function useUnfollowController() {
presenter,
);

const unfollowProfile = new UnfollowProfile(
signedUnfollow,
const delegableExecution = new DelegableSigning<UnfollowRequest>(
signedExecution,
gateway,
transactionQueue,
presenter,
);

const paidExecution = new PaidTransaction(activeWallet, gateway, presenter, transactionQueue);

const unfollowProfile = new UnfollowProfile(delegableExecution, paidExecution);

await unfollowProfile.execute(request);

return presenter.asResult();
Expand Down
Loading

0 comments on commit dd2f7d2

Please sign in to comment.