Skip to content

Commit

Permalink
Add support for meta-tx: sending join transaction on user's behalf
Browse files Browse the repository at this point in the history
  • Loading branch information
tfalencar committed Oct 17, 2023
1 parent 6e64b63 commit 3b9a7fb
Show file tree
Hide file tree
Showing 23 changed files with 575 additions and 296 deletions.
8 changes: 4 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ VITE_TERMS_OF_AGREEMENT_PATH=Legal/Terms
VITE_WALLET_CONNECT_BRIDGE_SERVER='https://r.bridge.walletconnect.org'

#####################################################################################
# Backend-only
# Backend-only - Service Provider Variables
SERVER_ETH_RPC='http://127.0.0.1:8545'

# Optional Redis connection string, otherwise defaults are used.
# Redis is used for delivery address/checkout persistance and pre-order steps.
# REDIS_CONNECTION_STRING=https://myServer:6379

# Service Provider Authorization Variables
# (For projects with signature-based join requirements)
AUTHORIZER_PRIVATE_KEY=0x0000
# (For projects with signature-based join enabled; smart contract's service provider signer != address(0)).
TX_SPONSOR_PRIVATE_KEY=0x0000 # Wallet used to sponsor gas on behalf of users (user acquisition cost).
AUTHORIZER_PRIVATE_KEY=0x0000 # Wallet used to authorize user wallets to join.
AUTHORIZER_SIGNATURE_EXPIRATION_TIME_IN_SECONDS=1800

#####################################################################################
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: AUTHORIZER_PRIVATE_KEY='dummy' AUTHORIZER_SIGNATURE_EXPIRATION_TIME_IN_SECONDS=1800 SERVER_ETH_RPC=https://example.com npm run build
# ATTENTION: The private keys below are 'dummy'/ well known. **Never** add your actual private keys here!
- run: TX_SPONSOR_PRIVATE_KEY='0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a' AUTHORIZER_PRIVATE_KEY='0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a' AUTHORIZER_SIGNATURE_EXPIRATION_TIME_IN_SECONDS=1800 SERVER_ETH_RPC=https://example.com npm run build
1 change: 1 addition & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ services:
- SERVER_ETH_RPC=${SERVER_ETH_RPC}
- VITE_DOMAIN=${VITE_DOMAIN} # signature generation and verification
- VITE_TERMS_OF_AGREEMENT_PATH=${VITE_TERMS_OF_AGREEMENT_PATH}
- TX_SPONSOR_PRIVATE_KEY=${TX_SPONSOR_PRIVATE_KEY}
- AUTHORIZER_PRIVATE_KEY=${AUTHORIZER_PRIVATE_KEY}
- AUTHORIZER_SIGNATURE_EXPIRATION_TIME_IN_SECONDS=${AUTHORIZER_SIGNATURE_EXPIRATION_TIME_IN_SECONDS}
- VITE_WALLET_CONNECT_RPC=${VITE_WALLET_CONNECT_RPC}
Expand Down
62 changes: 27 additions & 35 deletions src/lib/AddToCartView.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { blur } from 'svelte/transition';
import type { Readable } from 'svelte/store';
import { derived, type Readable } from 'svelte/store';
import {
initializeCampaignDynamicStores,
Expand All @@ -11,12 +11,8 @@
import { campaignStaticStores } from './Stores/campaignStaticDataStore.js';
import type {
CrowdtainerDynamicModel,
CrowdtainerStaticModel,
SplitSelection
} from '$lib/Model/CrowdtainerModel';
import { type UIFields, LoadStatus, loadingString } from '$lib/Converters/CrowdtainerData';
import type { CrowdtainerDynamicModel, SplitSelection } from '$lib/Model/CrowdtainerModel';
import { LoadStatus, loadingString } from '$lib/Converters/CrowdtainerData';
import { connected, accountAddress } from '$lib/Utils/wallet';
Expand All @@ -31,9 +27,12 @@
export let projectId: number;
export let staticDataLoadStatus: LoadStatus = LoadStatus.Loading;
let campaignStaticUI: UIFields | undefined;
let campaignStaticData: CrowdtainerStaticModel | undefined;
export const campaignStaticData = derived(campaignStaticStores, ($campaignStaticStores) => {
return $campaignStaticStores.staticData[projectId];
});
export const campaignStaticUI = derived(campaignStaticStores, ($campaignStaticStores) => {
return $campaignStaticStores.UIData[projectId];
});
let campaignDynamicData: Readable<CrowdtainerDynamicModel> | undefined;
let currentSelection = 0;
Expand All @@ -54,10 +53,10 @@
let userProductSelection = new Map<string, string>(); // <descriptor, sub-option value>
initializeDataForWallet(campaignStaticData?.contractAddress, $accountAddress);
initializeDataForWallet($campaignStaticData.contractAddress, $accountAddress);
function setUserSelection(descriptor: string, value: string) {
console.log(`Descriptor: ${descriptor}, Value: ${value}`);
// console.log(`Descriptor: ${descriptor}, Value: ${value}`);
userProductSelection.set(descriptor, value);
let categoryDescriptorIndex = productConfiguration.categoryDescriptors.indexOf(descriptor);
Expand All @@ -67,28 +66,28 @@
}
if (productConfiguration.categoryDescriptors.length !== userProductSelection.size) {
console.log('Missing user input selection');
console.log('No user product selection yet.');
return;
}
// Find Crowdtainer's index from selection
let delimiter = productConfiguration.categoryDelimiter;
campaignStaticUI?.descriptions.forEach((value, index) => {
$campaignStaticUI.descriptions.forEach((value, index) => {
let finalString = '';
userProductSelection.forEach((element) => {
finalString += `${element}${delimiter}`;
});
// drop last delimiter
finalString = finalString.substring(0, finalString.lastIndexOf(`${delimiter}`));
if (value === finalString) {
console.log(`Found match: ${finalString} at index ${index}`);
// console.log(`Found match: ${finalString} at index ${index}`);
currentSelection = index;
}
});
// Update current selection price
if (campaignStaticUI) {
currentPrice = campaignStaticUI.prices[currentSelection];
if ($campaignStaticUI) {
currentPrice = $campaignStaticUI.prices[currentSelection];
currentBasePrice = currentPrice / basePriceDenominator[currentSelection];
} else {
console.log(`Warning: prices not loaded yet.`);
Expand All @@ -104,20 +103,18 @@
} else {
campaignDynamicData = campaignDynamicStores.get(crowdtainerId);
}
if (campaignStaticUI === undefined) {
if ($campaignStaticUI === undefined) {
return;
}
updateCurrentSelection(0, campaignStaticUI.prices[0]);
console.log(`campaignStaticUI.descriptions: ${JSON.stringify(campaignStaticUI?.descriptions)}`);
updateCurrentSelection(0, $campaignStaticUI.prices[0]);
// products display
productTypes = new Set<string>();
productTypesIndices = new Set<number>();
descriptorForProduct = new Map<string, number>();
campaignStaticUI.descriptions.forEach((productLine) => {
$campaignStaticUI.descriptions.forEach((productLine) => {
let items = productLine.split(productConfiguration.categoryDelimiter);
if (items.length != productConfiguration.categoryDescriptors.length) {
let errorMessage = `
Expand Down Expand Up @@ -146,7 +143,7 @@
let result = [...descriptorForProduct.entries()].filter(
(value: [string, number]) => value[1] === index
);
console.log(`Found ${result[0]} ${result[1]} @ index ${index}`);
// console.log(`Found ${result[0]} ${result[1]} @ index ${index}`);
let productSuboptions = new Array<string>();
result.forEach((item) => {
productSuboptions.push(item[0]);
Expand All @@ -168,9 +165,6 @@
showToast(`Error fetching data: ${fetchError.details}`);
return;
}
campaignStaticData = $campaignStaticStores.staticData[projectId];
campaignStaticUI = $campaignStaticStores.UIData[projectId];
loadData();
});
Expand All @@ -181,7 +175,7 @@
}
function addProduct() {
if (staticDataLoadStatus === LoadStatus.FetchFailed || campaignStaticUI === undefined) {
if (staticDataLoadStatus === LoadStatus.FetchFailed || $campaignStaticUI === undefined) {
return;
}
let updatedQuantity = $joinSelection.get(crowdtainerId);
Expand All @@ -190,7 +184,7 @@
$joinSelection.set(crowdtainerId, updatedQuantity);
$joinSelection = $joinSelection;
} else {
let quantities: number[] = new Array<number>(campaignStaticUI.prices.length).fill(0);
let quantities: number[] = new Array<number>($campaignStaticUI.prices.length).fill(0);
quantities[currentSelection]++;
$joinSelection.set(crowdtainerId, quantities);
$joinSelection = $joinSelection;
Expand All @@ -202,20 +196,18 @@
// Immediatelly update UI elements related to connected wallet on wallet or connection change
$: $connected,
$accountAddress,
initializeDataForWallet(campaignStaticData?.contractAddress, $accountAddress);
$: campaignStaticUI, console.log(`campaignStaticUI: ${campaignStaticUI}`);
initializeDataForWallet($campaignStaticData.contractAddress, $accountAddress);
</script>

<div class="grid w-full gap-2 md:grid-cols-1">
<div class="md:pt-4">
<div class="text-black text-md font-medium dark:text-gray-200">Price</div>
{#if campaignStaticUI !== undefined && staticDataLoadStatus === LoadStatus.Loaded && currentPrice}
{#if staticDataLoadStatus === LoadStatus.Loaded && currentPrice}
{#key currentPrice}
<p in:blur|global={{ duration: 200 }} class="text-primary productPrice">
{currentPrice}
{campaignStaticUI.tokenSymbol} ({`${currentBasePrice.toFixed(2)} ${
campaignStaticUI.tokenSymbol
{$campaignStaticUI.tokenSymbol} ({`${currentBasePrice.toFixed(2)} ${
$campaignStaticUI.tokenSymbol
}/${basePriceUnit}`})
</p>
{/key}
Expand All @@ -225,7 +217,7 @@
</div>
<fieldset class="mt-4">
<legend class="sr-only">Choose a product</legend>
{#if campaignStaticUI}
{#if productOptions && staticDataLoadStatus === LoadStatus.Loaded}
<div class="flex flex-wrap">
{#each productOptions as productOption}
<div class="mx-0 max-w-md md:my-2">
Expand Down
8 changes: 3 additions & 5 deletions src/lib/CampaignActions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


import ModalDialog, {
ModalAnimation,
ModalIcon,
Expand Down Expand Up @@ -31,11 +29,11 @@ export async function callLeaveProject(vouchers721Address: string, crowdtainerAd
body: 'Your request to leave the project was not completed.',
icon: ModalIcon.Exclamation
});
console.log(`Failure!? ${signResult.unwrapErr()}`);
console.log(`Failure: ${signResult.unwrapErr()}`);
return;
}

showToast('You have successfully left the campaign.');
showToast('Successfully left the campaign.');

modalDialog.close();
onUserLeftCrowdtainer(crowdtainerAddress);
Expand All @@ -60,7 +58,7 @@ export async function callClaimFunds(crowdtainerAddress: string, modalDialog: Mo
body: 'Your request to leave the project was not completed.',
icon: ModalIcon.Exclamation
});
console.log(`Failure!? ${signResult.unwrapErr()}`);
console.log(`Failure: ${signResult.unwrapErr()}`);
return;
}

Expand Down
3 changes: 3 additions & 0 deletions src/lib/JoinProject.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
export let chainId: number;
export let tokenAddress: string;
export let tokenVersion: string;
export let txSponsoringEnabled: boolean;
export let vouchers721Address: string | undefined;
export let crowdtainerAddress: string;
export let basePriceDenominator: number[];
Expand Down Expand Up @@ -322,6 +323,7 @@
<JoinProjectButton
{tokenAddress}
{tokenVersion}
{txSponsoringEnabled}
{chainId}
{crowdtainerId}
{crowdtainerAddress}
Expand Down Expand Up @@ -426,6 +428,7 @@
<JoinProjectButton
{tokenAddress}
{tokenVersion}
{txSponsoringEnabled}
{chainId}
{crowdtainerId}
{crowdtainerAddress}
Expand Down
Loading

0 comments on commit 3b9a7fb

Please sign in to comment.