Skip to content

Commit

Permalink
Merge pull request #156 from desci-labs/orcid-write
Browse files Browse the repository at this point in the history
write DID to orcid + create API for orcid-did profile
  • Loading branch information
hubsmoke authored Nov 9, 2023
2 parents a93a2c0 + 8c89646 commit 4a53275
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 54 deletions.
1 change: 1 addition & 0 deletions desci-server/src/controllers/nodes/consent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const consent = async (req: Request, res: Response, next: NextFunction) =
ActionType.USER_TERMS_CONSENT,
{
...req.body,
email: user.email,
},
user?.id,
);
Expand Down
26 changes: 26 additions & 0 deletions desci-server/src/controllers/proxy/orcidProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,32 @@ interface OrcidQueryResponseError {
const ORCID_PROFILE_TTL = 1000 * 60 * 60 * 24; // 1 day
const REFRESH_RATE_LIMIT = 1000 * 60 * 5; // 5 minutes

export const orcidDid = async (req: Request, res: Response) => {
const wallet = await prisma.wallet.findFirst({
where: {
address: req.params.did,
},
});
if (!wallet) {
res.status(404).send({ ok: false });
return;
}
const user = await prisma.user.findFirst({
where: {
id: wallet.userId,
orcid: {
not: null,
},
},
});
if (!user) {
res.status(404).send({ ok: false });
return;
}
res.send({ orcid: user.orcid, did: wallet.address });
return;
};

export const orcidProfile = async (
req: Request,
res: Response<OrcidQueryResponse | OrcidQueryResponseError>,
Expand Down
202 changes: 151 additions & 51 deletions desci-server/src/controllers/users/associateWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ErrorTypes, SiweMessage } from 'siwe';
import prisma from 'client';
import parentLogger from 'logger';
import { saveInteraction } from 'services/interactionLog';
import { writeExternalIdToOrcidProfile } from 'services/user';

const createWalletNickname = async (user: Prisma.UserWhereInput) => {
const count = await prisma.wallet.count({
Expand All @@ -19,6 +20,89 @@ const createWalletNickname = async (user: Prisma.UserWhereInput) => {
return `Account #${count + 1}`;
};

export const associateOrcidWallet = async (req: Request, res: Response, next: NextFunction) => {
const logger = parentLogger.child({
module: 'USERS::softAssociateWalletController',
user: (req as any).user,
body: req.body,
});

// associate without siwe check (only necessary for direct DID login, which is unsupported for ORCID DIDs right now)
try {
const user = (req as any).user;
const { did } = req.body;
if (!did) {
res.status(400).send({ err: 'missing wallet address' });
return;
}
const doesExist =
(await prisma.wallet.count({
where: {
address: did,
},
})) > 0;
if (doesExist) {
res.status(400).send({ err: 'duplicate DID (global)' });
return;
}
const ORCID_NICKNAME = 'ORCID';
const hasOrcidWallet = await prisma.wallet.count({
where: {
user,
nickname: ORCID_NICKNAME,
},
});
if (hasOrcidWallet > 0) {
res.status(400).send({ err: 'orcid DID already registered' });
return;
}

try {
const addWallet = await prisma.wallet.create({
data: { address: did, userId: user.id, nickname: ORCID_NICKNAME },
});
saveInteraction(
req,
ActionType.USER_WALLET_ASSOCIATE,
{
addr: did,
orcid: req.body.orcid,
orcidUser: user.orcid,
},
user.id,
);
// check if orcid associated
// add to orcid profile in the did:pkh format
// did:pkh:eip155:1:0xb9c5714089478a327f09197987f16f9e5d936e8a
// POST to orcid API as an External Identifier
if (user.orcid) {
console.log('adding to orcid profile');
await writeExternalIdToOrcidProfile(user.id, did);
console.log('done writing');
}

try {
const hash = await sendGiftTxn(user, did, addWallet.id);
res.send({ ok: true, gift: hash });
return;
} catch (err) {
logger.error({ err }, 'Error sending orcid DID txn');
}
res.send({ ok: true });
return;
// req.session.save(() => res.status(200).send({ ok: true }));
} catch (err) {
logger.error({ err }, 'Error associating orcid DID to user #1');
res.status(500).send({ err });
return;
}
} catch (e) {
logger.error({ err: e }, 'Error associating orcid DID to user #2');

res.status(500).json({ message: e.message });
}
};

export const associateWallet = async (req: Request, res: Response, next: NextFunction) => {
const logger = parentLogger.child({
module: 'USERS::associateWalletController',
Expand Down Expand Up @@ -72,57 +156,12 @@ export const associateWallet = async (req: Request, res: Response, next: NextFun
},
user.id,
);
if (process.env.HOT_WALLET_KEY) {
/**
* Auto send user ETH
*/
const giftedWallets = await prisma.wallet.count({
where: {
user,
giftTransaction: { not: null },
},
});
if (giftedWallets === 0) {
let provider;
try {
provider = new ethers.providers.JsonRpcProvider(
process.env.NODE_ENV === 'production'
? 'https://eth-goerli.g.alchemy.com/v2/ZeIzCAJyPpRnTtPNSmddHGF-q2yp-2Uy'
: 'http://host.docker.internal:8545',
);
const bn = await provider.getBlockNumber();

const privateKey = process.env.HOT_WALLET_KEY;
const wallet = new ethers.Wallet(privateKey, provider);
const receiverAddress = walletAddress;
// Ether amount to send
const amountInEther = '0.005';
// Create a transaction object
const tx = {
to: receiverAddress,
// Convert currency unit from ether to wei
value: ethers.utils.parseEther(amountInEther),
};

// Send a transaction
const txObj = await wallet.sendTransaction(tx);

await prisma.wallet.update({
where: {
id: addWallet.id,
},
data: {
giftTransaction: txObj.hash,
usedFaucet: true,
},
});
logger.info(`gifted user id ${user.id} txHash ${txObj.hash}`);
res.send({ ok: true, gift: txObj.hash });
return;
} catch (err) {
logger.error({ err }, 'failed to connect to blockchain RPC, sending funds failed');
}
}

try {
const hash = await sendGiftTxn(user, walletAddress, addWallet.id);
res.send({ ok: true, gift: hash });
} catch (err) {
logger.error({ err }, 'Error sending gift txn');
}
res.send({ ok: true });
// req.session.save(() => res.status(200).send({ ok: true }));
Expand Down Expand Up @@ -156,3 +195,64 @@ export const associateWallet = async (req: Request, res: Response, next: NextFun
}
}
};

const sendGiftTxn = async (user: User, walletAddress: string, addedWalletId: number) => {
const logger = parentLogger.child({
module: 'USERS::associateWallet::sendGiftTxn',
user,
walletAddress,
});
if (process.env.HOT_WALLET_KEY) {
/**
* Auto send user ETH
*/
const giftedWallets = await prisma.wallet.count({
where: {
user,
giftTransaction: { not: null },
},
});
if (giftedWallets === 0) {
let provider;
try {
provider = new ethers.providers.JsonRpcProvider(
process.env.NODE_ENV === 'production'
? 'https://eth-goerli.g.alchemy.com/v2/ZeIzCAJyPpRnTtPNSmddHGF-q2yp-2Uy'
: 'http://host.docker.internal:8545',
);
const bn = await provider.getBlockNumber();

const privateKey = process.env.HOT_WALLET_KEY;
const wallet = new ethers.Wallet(privateKey, provider);
const receiverAddress = walletAddress;
// Ether amount to send
const amountInEther = '0.005';
// Create a transaction object
const tx = {
to: receiverAddress,
// Convert currency unit from ether to wei
value: ethers.utils.parseEther(amountInEther),
};

// Send a transaction
const txObj = await wallet.sendTransaction(tx);

await prisma.wallet.update({
where: {
id: addedWalletId,
},
data: {
giftTransaction: txObj.hash,
usedFaucet: true,
},
});
logger.info(`gifted user id ${user.id} txHash ${txObj.hash}`);

return txObj.hash;
} catch (err) {
logger.error({ err }, 'failed to connect to blockchain RPC, sending funds failed');
}
}
}
return null;
};
3 changes: 2 additions & 1 deletion desci-server/src/routes/v1/services.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Router } from 'express';

import { orcidProfile } from 'controllers/proxy/orcidProfile';
import { orcidDid, orcidProfile } from 'controllers/proxy/orcidProfile';

const router = Router();

router.get('/orcid/profile/:orcidId', [], orcidProfile);
router.get('/orcid/did/:did', [], orcidDid);

export default router;
3 changes: 2 additions & 1 deletion desci-server/src/routes/v1/users.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Router } from 'express';

import { list, associateWallet, updateProfile } from 'controllers/users';
import { list, associateWallet, updateProfile, associateOrcidWallet } from 'controllers/users';
import { usage } from 'controllers/users/usage';
import { checkJwt } from 'middleware/checkJwt';
import { ensureAdmin } from 'middleware/ensureAdmin';
Expand All @@ -11,6 +11,7 @@ const router = Router();
router.get('/usage', [ensureUser], usage);
router.get('/', [ensureUser, ensureAdmin], list);
router.post('/associate', [ensureUser], associateWallet);
router.post('/orcid/associate', [ensureUser], associateOrcidWallet);
router.patch('/updateProfile', [ensureUser], updateProfile);

export default router;
59 changes: 58 additions & 1 deletion desci-server/src/services/user.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ActionType, AuthToken, AuthTokenSource, User, prisma } from '@prisma/client';
import axios from 'axios';

import { OrcIdRecordData, generateAccessToken, getOrcidRecord } from 'controllers/auth';
import parentLogger from 'logger';
Expand Down Expand Up @@ -64,6 +65,63 @@ export async function isAuthTokenSetForUser(userId: number): Promise<boolean> {
return !!authToken;
}

export async function writeExternalIdToOrcidProfile(userId: number, didAddress: string) {
const user = await client.user.findFirst({
where: {
id: userId,
},
});
if (!user.orcid) {
throw new Error('User does not have an orcid');
}
const authToken = await client.authToken.findFirst({
where: {
userId,
source: AuthTokenSource.ORCID,
},
});
if (!authToken) {
throw new Error('User does not have an orcid auth token');
}
// check if it's already written to orcid
const headers = {
'Content-Type': 'application/vnd.orcid+json',
Authorization: `Bearer ${authToken.accessToken}`,
};
const orcidId = user.orcid;
const fullDid = `did:pkh:eip155:1:${didAddress}`;
try {
const externalIds = await axios.get(
`https://api.${process.env.ORCID_API_DOMAIN}/v3.0/${orcidId}/external-identifiers`,
{ headers },
);
if (externalIds.data['external-identifier'].some((id) => id['external-id-value'] === fullDid)) {
console.log('External ID already added');
return;
}
debugger;
} catch (error) {
console.error('Error getting external IDs:', error.response?.data || error.message);
}

const apiUrl = `https://api.${process.env.ORCID_API_DOMAIN}/v3.0/${orcidId}/external-identifiers`;
const externalIdPayload = {
'external-id-type': 'Public Key',
'external-id-value': fullDid,
'external-id-url': {
value: `https://nodes.desci.com/orcid-did/${didAddress}`,
},
'external-id-relationship': 'self',
};

try {
const response = await axios.post(apiUrl, externalIdPayload, { headers });
console.log('External ID added:', response.data);
} catch (error) {
console.error('Error adding external ID:', error.response?.data || error.message);
}
}

export async function connectOrcidToUserIfPossible(
userId: number,
orcid: string,
Expand All @@ -72,7 +130,6 @@ export async function connectOrcidToUserIfPossible(
expiresIn: number,
orcidLookup: (orcid: string, accessToken: string) => Promise<OrcIdRecordData> = getOrcidRecord,
) {
debugger;
logger.info({ fn: 'connectOrcidToUserIfPossible', orcid, accessTokenPresent: !!accessToken }, `doing orcid lookup`);
const orcidRecord = await orcidLookup(orcid, accessToken);
logger.info({ fn: 'connectOrcidToUserIfPossible', orcidRecord, orcid }, `found orcid record`);
Expand Down

0 comments on commit 4a53275

Please sign in to comment.