Skip to content

Custodial account UI #471

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions app/src/api/lit.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import * as LIT from 'types/generated/lit-sessions_pb';
import * as ACCOUNT from 'types/generated/lit-accounts_pb';
import * as SESSION from 'types/generated/lit-sessions_pb';
import { Accounts } from 'types/generated/lit-accounts_pb_service';
import { Sessions } from 'types/generated/lit-sessions_pb_service';
import { b64 } from 'util/strings';
import { MAX_DATE } from 'util/constants';
import BaseApi from './base';
import GrpcClient from './grpc';

Expand All @@ -16,24 +19,46 @@ class LitApi extends BaseApi<LitEvents> {
this._grpc = grpc;
}

/**
* call the Lit `CreateAccount` RPC and return the response
*/
async createAccount(
accountBalance: number,
expirationDate: Date,
): Promise<ACCOUNT.CreateAccountResponse.AsObject> {
const req = new ACCOUNT.CreateAccountRequest();
req.setAccountBalance(accountBalance.toString());

if (expirationDate === MAX_DATE) {
req.setExpirationDate('0');
} else {
req.setExpirationDate(Math.floor(expirationDate.getTime() / 1000).toString());
}

const res = await this._grpc.request(Accounts.CreateAccount, req, this._meta);
return res.toObject();
}

/**
* call the Lit `AddSession` RPC and return the response
*/
async addSession(
label: string,
sessionType: LIT.SessionTypeMap[keyof LIT.SessionTypeMap],
sessionType: SESSION.SessionTypeMap[keyof SESSION.SessionTypeMap],
expiry: Date,
mailboxServerAddr: string,
devServer: boolean,
macaroonCustomPermissions: Array<LIT.MacaroonPermission>,
): Promise<LIT.AddSessionResponse.AsObject> {
const req = new LIT.AddSessionRequest();
macaroonCustomPermissions: Array<SESSION.MacaroonPermission>,
accountId: string,
): Promise<SESSION.AddSessionResponse.AsObject> {
const req = new SESSION.AddSessionRequest();
req.setLabel(label);
req.setSessionType(sessionType);
req.setExpiryTimestampSeconds(Math.floor(expiry.getTime() / 1000).toString());
req.setMailboxServerAddr(mailboxServerAddr);
req.setDevServer(devServer);
req.setMacaroonCustomPermissionsList(macaroonCustomPermissions);
req.setAccountId(accountId);

const res = await this._grpc.request(Sessions.AddSession, req, this._meta);
return res.toObject();
Expand All @@ -42,8 +67,8 @@ class LitApi extends BaseApi<LitEvents> {
/**
* call the Lit `ListSessions` RPC and return the response
*/
async listSessions(): Promise<LIT.ListSessionsResponse.AsObject> {
const req = new LIT.ListSessionsRequest();
async listSessions(): Promise<SESSION.ListSessionsResponse.AsObject> {
const req = new SESSION.ListSessionsRequest();
const res = await this._grpc.request(Sessions.ListSessions, req, this._meta);
return res.toObject();
}
Expand All @@ -53,8 +78,8 @@ class LitApi extends BaseApi<LitEvents> {
*/
async revokeSession(
localPublicKey: string,
): Promise<LIT.RevokeSessionResponse.AsObject> {
const req = new LIT.RevokeSessionRequest();
): Promise<SESSION.RevokeSessionResponse.AsObject> {
const req = new SESSION.RevokeSessionRequest();
req.setLocalPublicKey(b64(localPublicKey));
const res = await this._grpc.request(Sessions.RevokeSession, req, this._meta);
return res.toObject();
Expand Down
3 changes: 3 additions & 0 deletions app/src/components/common/FormInputNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface Props {
value?: number;
extra?: ReactNode;
placeholder?: string;
className?: string;
onChange: (value: number) => void;
}

Expand All @@ -15,6 +16,7 @@ const FormInputNumber: React.FC<Props> = ({
value,
extra,
placeholder,
className,
onChange,
}) => {
const handleChange = useCallback(
Expand All @@ -35,6 +37,7 @@ const FormInputNumber: React.FC<Props> = ({

return (
<FormInput
className={className}
label={label}
value={valueText}
extra={extra}
Expand Down
30 changes: 30 additions & 0 deletions app/src/components/connect/CustomSessionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Paragraph, Small, Label } from 'components/common/v2/Text';
import OverlayFormWrap from 'components/common/OverlayFormWrap';
import FormField from 'components/common/FormField';
import FormInput from 'components/common/FormInput';
import FormInputNumber from 'components/common/FormInputNumber';
import FormSelect from 'components/common/FormSelect';
import FormDate from 'components/common/FormDate';
import FormSwitch from 'components/common/v2/FormSwitch';
Expand Down Expand Up @@ -65,6 +66,11 @@ const Styled = {
padding: 12px 40px 12px 0px;
}
`,
FormInputNumber: styled(FormInputNumber)`
input {
background-color: ${props => props.theme.colors.lightningNavy};
}
`,
FormDate: styled(FormDate)`
input {
font-family: ${props => props.theme.fonts.open.regular};
Expand Down Expand Up @@ -122,6 +128,7 @@ const CustomSessionPage: React.FC = () => {
Permission,
FormSelect,
FormInput,
FormInputNumber,
FormDate,
Small,
Button,
Expand Down Expand Up @@ -199,6 +206,14 @@ const CustomSessionPage: React.FC = () => {
<Small>{l('paymentsDesc')}</Small>
</PermissionType>

<PermissionType
active={addSessionView.permissionType === 'custodial'}
onClick={setPermissionType('custodial')}
>
<Paragraph bold>{l('custodial')}</Paragraph>
<Small>{l('custodialDesc')}</Small>
</PermissionType>

<PermissionType
active={addSessionView.permissionType === 'custom'}
onClick={setPermissionType('custom')}
Expand All @@ -213,6 +228,21 @@ const CustomSessionPage: React.FC = () => {
<Label semiBold>Permissions</Label>

<Permissions>
{addSessionView.permissionType === 'custodial' && (
<FormField>
<Label semiBold space={8}>
{l('addBalance')}
</Label>

<FormInputNumber
value={addSessionView.custodialBalance}
onChange={addSessionView.setCustodialBalance}
placeholder="1,000,000"
extra={<b>sats</b>}
/>
</FormField>
)}

<Permission>
<div>
<Paragraph bold>{l('permView')}</Paragraph>
Expand Down
13 changes: 13 additions & 0 deletions app/src/components/connect/SessionRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Session } from 'store/models';
import { BoltOutlined, Close, Column, Copy, QRCode, Row } from 'components/base';
import SortableHeader from 'components/common/SortableHeader';
import Tip from 'components/common/Tip';
import * as LIT from 'types/generated/lit-sessions_pb';
import QRCodeModal from './QRCodeModal';

/**
Expand Down Expand Up @@ -151,6 +152,18 @@ const SessionRow: React.FC<Props> = ({ session, style }) => {
<QRCode disabled />
</Tip>
</>
) : session.type === LIT.SessionType.TYPE_MACAROON_ACCOUNT ? (
<>
<Tip overlay={l('pairCustodial')}>
<BoltOutlined disabled />
</Tip>
<Tip overlay={l('copy')}>
<Copy onClick={handleCopy} />
</Tip>
<Tip overlay={l('generateQR')}>
<QRCode onClick={toggleQRModal} />
</Tip>
</>
) : (
<>
<Tip overlay={l('pairTerminal')}>
Expand Down
4 changes: 4 additions & 0 deletions app/src/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@
"cmps.connect.CustomSessionPage.liquidityDesc": "User can only set fees, use Loop, and use Pool.",
"cmps.connect.CustomSessionPage.payments": "Payments Manager",
"cmps.connect.CustomSessionPage.paymentsDesc": "User can only send and receive payments.",
"cmps.connect.CustomSessionPage.custodial": "Custodial Account",
"cmps.connect.CustomSessionPage.custodialDesc": "Create a custodial off-chain account for your node.",
"cmps.connect.CustomSessionPage.custom": "Custom",
"cmps.connect.CustomSessionPage.customDesc": "Create a session with fully custom permissions.",
"cmps.connect.CustomSessionPage.permissions": "Permissions",
"cmps.connect.CustomSessionPage.addBalance": "Add Balance",
"cmps.connect.CustomSessionPage.permView": "View Activity",
"cmps.connect.CustomSessionPage.permViewDesc": "See node history and activity.",
"cmps.connect.CustomSessionPage.permOpen": "Open Channel",
Expand Down Expand Up @@ -86,6 +89,7 @@
"cmps.connect.SessionRow.pairTerminal": "Pair with Lightning Terminal",
"cmps.connect.SessionRow.generateQR": "Generate QR Code",
"cmps.connect.SessionRow.revoke": "Revoke Session",
"cmps.connect.SessionRow.pairCustodial": "Custodial accounts can not be used with Terminal",
"cmps.connect.QRCodeModal.title": "LNC QR",
"cmps.connect.QRCodeModal.desc": "Scan to connect to Terminal from your mobile phone.",
"cmps.home.HomePage.pageTitle": "Home",
Expand Down
4 changes: 3 additions & 1 deletion app/src/store/models/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export default class Session {
return 'Admin';
case LIT.SessionType.TYPE_MACAROON_CUSTOM:
return 'Custom';
case LIT.SessionType.TYPE_MACAROON_ACCOUNT:
return 'Custodial';
case LIT.SessionType.TYPE_UI_PASSWORD:
return 'LiT UI Password';
}
Expand Down Expand Up @@ -96,7 +98,7 @@ export default class Session {

/** The HEX encoded pairing secret mnemonic and mailbox server address */
get encodedPairingData() {
const data = `${this.pairingSecretMnemonic}||${this.mailboxServerAddr}`;
const data = `${this.pairingSecretMnemonic}||${this.mailboxServerAddr}||${this.typeLabel}`;
return Buffer.from(data, 'ascii').toString('base64');
}

Expand Down
2 changes: 2 additions & 0 deletions app/src/store/stores/sessionStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export default class SessionStore {
copy = false,
proxy?: string,
customPermissions?: LIT.MacaroonPermission[],
accountId?: string,
) {
try {
this._store.log.info(`submitting session with label ${label}`, {
Expand All @@ -119,6 +120,7 @@ export default class SessionStore {
proxy || this.proxyServer,
!IS_PROD,
customPermissions || [],
accountId || '',
);

// fetch all sessions to update the store's state
Expand Down
43 changes: 42 additions & 1 deletion app/src/store/views/addSessionView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default class AddSessionView {
private _store: Store;

label = '';
permissionType = 'admin'; // Expected values: admin | read-only | custom | liquidity | payments
permissionType = 'admin'; // Expected values: admin | read-only | custodial | custom | liquidity | payments
editing = false;
permissions: { [key: string]: boolean } = {
openChannel: false,
Expand All @@ -30,6 +30,7 @@ export default class AddSessionView {
expirationDate = '';
showAdvanced = false;
proxy = '';
custodialBalance = 0;

constructor(store: Store) {
makeAutoObservable(
Expand All @@ -52,6 +53,8 @@ export default class AddSessionView {
return LIT.SessionType.TYPE_MACAROON_ADMIN;
} else if (this.permissionType === 'read-only') {
return LIT.SessionType.TYPE_MACAROON_READONLY;
} else if (this.permissionType === 'custodial') {
return LIT.SessionType.TYPE_MACAROON_ACCOUNT;
}

return LIT.SessionType.TYPE_MACAROON_CUSTOM;
Expand Down Expand Up @@ -125,6 +128,10 @@ export default class AddSessionView {
this.proxy = proxy;
}

setCustodialBalance(balance: number) {
this.custodialBalance = balance;
}

setPermissionType(permissionType: string) {
this.permissionType = permissionType;

Expand All @@ -150,6 +157,12 @@ export default class AddSessionView {
this.permissions.receive = true;
break;

case 'custodial':
this.setAllPermissions(false);
this.permissions.send = true;
this.permissions.receive = true;
break;

case 'custom':
// We don't need to change anything, let the user customize permissions how they want
break;
Expand Down Expand Up @@ -203,19 +216,32 @@ export default class AddSessionView {

async handleCustomSubmit() {
let label = this.label;
let accountId = '';

// Automatically generate human friendly labels for custom sessions
if (label === '') {
label = `My ${this.permissionType} session`;
}

if (this.permissionType === 'custodial') {
const custodialAccountId = await this.registerCustodialAccount();

// Return immediately to prevent a session being created when there is an error creating the custodial account
if (!custodialAccountId) {
return;
}

accountId = custodialAccountId;
}

const session = await this._store.sessionStore.addSession(
label,
this.sessionType,
this.sessionDate,
true,
this.sessionProxy,
this.getMacaroonPermissions,
accountId,
);

if (session) {
Expand All @@ -224,6 +250,21 @@ export default class AddSessionView {
}
}

async registerCustodialAccount(): Promise<string | undefined> {
try {
const response = await this._store.api.lit.createAccount(
this.custodialBalance,
this.sessionDate,
);

if (response.account) {
return response.account.id;
}
} catch (error) {
this._store.appView.handleError(error, 'Unable to register custodial account');
}
}

//
// Private helper functions
//
Expand Down