Implement payments for app deployments #17

Merged
nabarun merged 27 commits from iv-integrate-payments into main 2024-10-28 09:46:19 +00:00
3 changed files with 172 additions and 36 deletions
Showing only changes of commit e1f5fbab3c - Show all commits

View File

@ -57,4 +57,10 @@ export interface Config {
gitHub: GitHubConfig;
registryConfig: RegistryConfig;
auction: AuctionConfig;
turnkey: {
apiBaseUrl: string;
apiPublicKey: string;
apiPrivateKey: string;
defaultOrganizationId: string;
};
}

View File

@ -1,50 +1,50 @@
import { Router } from 'express';
import { SiweMessage } from 'siwe';
import { Service } from '../service';
// import { authenticateUser, createUser } from '../turnkey-backend';
import { authenticateUser, createUser } from '../turnkey-backend';
const router = Router();
//
// Turnkey
//
// router.get('/registration/:email', async (req, res) => {
// const service: Service = req.app.get('service');
// const user = await service.getUserByEmail(req.params.email);
// if (user) {
// return res.send({ subOrganizationId: user?.subOrgId });
// } else {
// return res.sendStatus(204);
// }
// });
router.get('/registration/:email', async (req, res) => {
const service: Service = req.app.get('service');
const user = await service.getUserByEmail(req.params.email);
if (user) {
return res.send({ subOrganizationId: user?.subOrgId });
} else {
return res.sendStatus(204);
}
});
// router.post('/register', async (req, res) => {
// console.log('Register', req.body);
// const { email, challenge, attestation } = req.body;
// const user = await createUser(req.app.get('service'), {
// challenge,
// attestation,
// userEmail: email,
// userName: email.split('@')[0],
// });
// req.session.address = user.id;
// res.sendStatus(200);
// });
router.post('/register', async (req, res) => {
console.log('Register', req.body);
const { email, challenge, attestation } = req.body;
const user = await createUser(req.app.get('service'), {
challenge,
attestation,
userEmail: email,
userName: email.split('@')[0],
});
req.session.address = user.id;
res.sendStatus(200);
});
// router.post('/authenticate', async (req, res) => {
// console.log('Authenticate', req.body);
// const { signedWhoamiRequest } = req.body;
// const user = await authenticateUser(
// req.app.get('service'),
// signedWhoamiRequest,
// );
// if (user) {
// req.session.address = user.id;
// res.sendStatus(200);
// } else {
// res.sendStatus(401);
// }
// });
router.post('/authenticate', async (req, res) => {
console.log('Authenticate', req.body);
const { signedWhoamiRequest } = req.body;
const user = await authenticateUser(
req.app.get('service'),
signedWhoamiRequest,
);
if (user) {
req.session.address = user.id;
res.sendStatus(200);
} else {
res.sendStatus(401);
}
});
//
// SIWE Auth

View File

@ -0,0 +1,130 @@
import { Turnkey, TurnkeyApiTypes } from '@turnkey/sdk-server';
// Default path for the first Ethereum address in a new HD wallet.
// See https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki, paths are in the form:
// m / purpose' / coin_type' / account' / change / address_index
// - Purpose is a constant set to 44' following the BIP43 recommendation.
// - Coin type is set to 60 (ETH) -- see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
// - Account, Change, and Address Index are set to 0
import { DEFAULT_ETHEREUM_ACCOUNTS } from '@turnkey/sdk-server';
import { getConfig } from './utils';
import { Service } from './service';
type TAttestation = TurnkeyApiTypes['v1Attestation'];
type CreateUserParams = {
userName: string;
userEmail: string;
challenge: string;
attestation: TAttestation;
};
export async function createUser(
service: Service,
{ userName, userEmail, challenge, attestation }: CreateUserParams,
) {
try {
if (await service.getUserByEmail(userEmail)) {
throw new Error(`User already exists: ${userEmail}`);
}
const config = await getConfig();
const turnkey = new Turnkey(config.turnkey);
const apiClient = turnkey.api();
const walletName = `Default ETH Wallet`;
const createSubOrgResponse = await apiClient.createSubOrganization({
subOrganizationName: `Default SubOrg for ${userEmail}`,
rootQuorumThreshold: 1,
rootUsers: [
{
userName,
userEmail,
apiKeys: [],
authenticators: [
{
authenticatorName: 'Passkey',
challenge,
attestation,
},
],
},
],
wallet: {
walletName: walletName,
accounts: DEFAULT_ETHEREUM_ACCOUNTS,
},
});
const subOrgId = refineNonNull(createSubOrgResponse.subOrganizationId);
const wallet = refineNonNull(createSubOrgResponse.wallet);
const result = {
id: wallet.walletId,
address: wallet.addresses[0],
subOrgId: subOrgId,
};
console.log('Turnkey success', result);
const user = await service.createUser({
name: userName,
email: userEmail,
subOrgId,
ethAddress: wallet.addresses[0],
turnkeyWalletId: wallet.walletId,
});
console.log('New user', user);
return user;
} catch (e) {
console.error('Failed to create user:', e);
throw e;
}
}
export async function authenticateUser(
service: Service,
signedWhoamiRequest: {
url: string;
body: any;
stamp: {
stampHeaderName: string;
stampHeaderValue: string;
};
},
) {
try {
const tkRes = await fetch(signedWhoamiRequest.url, {
method: 'POST',
body: signedWhoamiRequest.body,
headers: {
[signedWhoamiRequest.stamp.stampHeaderName]:
signedWhoamiRequest.stamp.stampHeaderValue,
},
});
console.log('AUTH RESULT', tkRes.status);
if (tkRes.status !== 200) {
console.log(await tkRes.text());
return null;
}
const orgId = (await tkRes.json()).organizationId;
const user = await service.getUserBySubOrgId(orgId);
return user;
} catch (e) {
console.error('Failed to authenticate:', e);
throw e;
}
}
function refineNonNull<T>(
input: T | null | undefined,
errorMessage?: string,
): T {
if (input == null) {
throw new Error(errorMessage ?? `Unexpected ${JSON.stringify(input)}`);
}
return input;
}