Skip to content

Commit

Permalink
Initial version (spruceid#1)
Browse files Browse the repository at this point in the history
* Client to request signature and validate with ABNF

Co-authored-by: K. Rhoda <[email protected]>

* Check ENS

* Add editor config

* Wallet connect integration

* Disconnect current provider to avoid Infura requests

* Use Promise and events

* Bump version

Co-authored-by: Simon Bihel <[email protected]>
Co-authored-by: K. Rhoda <[email protected]>
  • Loading branch information
3 people authored Oct 26, 2021
1 parent 9b7db43 commit 3a018b2
Show file tree
Hide file tree
Showing 10 changed files with 24,160 additions and 5 deletions.
17 changes: 17 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true

[*.{ts}]
charset = utf-8

[Makefile]
indent_style = tab

[lib/**.ts]
indent_style = tab
indent_size = 4
max_line_length = 120
quote_type = single
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
5 changes: 5 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
32 changes: 32 additions & 0 deletions lib/abnf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ParsedMessage } from './abnf';

describe('ABNF Client', () => {
it('Parses message successfully', () => {
const msg = `service.org wants you to sign in with your Ethereum account:
0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
I accept the ServiceOrg Terms of Service: https://service.org/tos
URI: https://service.org/login
Version: 1
Nonce: 32891757
Issued At: 2021-09-30T16:25:24Z
Chain ID: 1
Resources:
- ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu
- https://example.com/my-web2-claim.json`;
const parsedMessage = new ParsedMessage(msg);
expect(parsedMessage.domain).toBe('service.org');
expect(parsedMessage.address).toBe('0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2');
expect(parsedMessage.statement).toBe('I accept the ServiceOrg Terms of Service: https://service.org/tos');
expect(parsedMessage.uri).toBe('https://service.org/login');
expect(parsedMessage.version).toBe('1');
expect(parsedMessage.nonce).toBe('32891757');
expect(parsedMessage.issuedAt).toBe('2021-09-30T16:25:24Z');
expect(parsedMessage.chainId).toBe('1');
expect(parsedMessage.resources).toStrictEqual([
'ipfs://Qme7ss3ARVgxv6rXqVPiikMJ8u2NLgmgszg13pYrDKEoiu',
'https://example.com/my-web2-claim.json',
]);
});
});
290 changes: 290 additions & 0 deletions lib/abnf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
import { apgApi, apgLib } from 'apg-js';

const GRAMMAR = `
sign-in-with-ethereum =
domain %s" wants you to sign in with your Ethereum account:" LF
address LF
LF
[ statement LF ]
LF
%s"URI: " URI LF
%s"Version: " version LF
%s"Nonce: " nonce LF
%s"Issued At: " issued-at LF
[ %s"Expiration Time: " expiration-time LF ]
[ %s"Not Before: " not-before LF ]
[ %s"Request ID: " request-id LF ]
[ %s"Chain ID: " chain-id LF ]
[ %s"Resources:" LF
resources ]
domain = dnsauthority
address = "0x" 40*40HEXDIG
; Must also conform to captilization
; checksum encoding specified in EIP-55
; where applicable (EOAs).
statement = *( reserved / unreserved / " " )
; The purpose is to exclude LF (line breaks).
version = "1"
nonce = 8*( ALPHA / DIGIT )
issued-at = date-time
expiration-time = date-time
not-before = date-time
request-id = *pchar
chain-id = 1*DIGIT
; See EIP-155 for valid CHAIN_IDs.
resources = resource *( LF resource )
resource = "- " URI
; ------------------------------------------------------------------------------
; RFC 3986
URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ]
hier-part = "//" authority path-abempty
/ path-absolute
/ path-rootless
/ path-empty
scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." )
authority = [ userinfo "@" ] host [ ":" port ]
userinfo = *( unreserved / pct-encoded / sub-delims / ":" )
host = IP-literal / IPv4address / reg-name
port = *DIGIT
IP-literal = "[" ( IPv6address / IPvFuture ) "]"
IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" )
IPv6address = 6( h16 ":" ) ls32
/ "::" 5( h16 ":" ) ls32
/ [ h16 ] "::" 4( h16 ":" ) ls32
/ [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32
/ [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32
/ [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32
/ [ *4( h16 ":" ) h16 ] "::" ls32
/ [ *5( h16 ":" ) h16 ] "::" h16
/ [ *6( h16 ":" ) h16 ] "::"
h16 = 1*4HEXDIG
ls32 = ( h16 ":" h16 ) / IPv4address
IPv4address = dec-octet "." dec-octet "." dec-octet "." dec-octet
dec-octet = DIGIT ; 0-9
/ %x31-39 DIGIT ; 10-99
/ "1" 2DIGIT ; 100-199
/ "2" %x30-34 DIGIT ; 200-249
/ "25" %x30-35 ; 250-255
reg-name = *( unreserved / pct-encoded / sub-delims )
path-abempty = *( "/" segment )
path-absolute = "/" [ segment-nz *( "/" segment ) ]
path-rootless = segment-nz *( "/" segment )
path-empty = 0pchar
segment = *pchar
segment-nz = 1*pchar
pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
query = *( pchar / "/" / "?" )
fragment = *( pchar / "/" / "?" )
pct-encoded = "%" HEXDIG HEXDIG
unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
reserved = gen-delims / sub-delims
gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
/ "*" / "+" / "," / ";" / "="
; ------------------------------------------------------------------------------
; RFC 4501
dnsauthority = host [ ":" port ]
; See RFC 3986 for the
; definition of "host" and "port".
; ------------------------------------------------------------------------------
; RFC 3339
date-fullyear = 4DIGIT
date-month = 2DIGIT ; 01-12
date-mday = 2DIGIT ; 01-28, 01-29, 01-30, 01-31 based on
; month/year
time-hour = 2DIGIT ; 00-23
time-minute = 2DIGIT ; 00-59
time-second = 2DIGIT ; 00-58, 00-59, 00-60 based on leap second
; rules
time-secfrac = "." 1*DIGIT
time-numoffset = ("+" / "-") time-hour ":" time-minute
time-offset = "Z" / time-numoffset
partial-time = time-hour ":" time-minute ":" time-second
[time-secfrac]
full-date = date-fullyear "-" date-month "-" date-mday
full-time = partial-time time-offset
date-time = full-date "T" full-time
; ------------------------------------------------------------------------------
; RFC 5234
ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
LF = %x0A
; linefeed
DIGIT = %x30-39
; 0-9
HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F"
`;

export class ParsedMessage {
domain: string;
address: string;
statement: string;
uri: string;
version: string;
nonce: string;
chainId: string | null;
issuedAt: string | null;
expirationTime: string | null;
notBefore: string | null;
requestId: string | null;
resources: Array<string> | null;

constructor(msg: string) {
const api = new apgApi(GRAMMAR);
api.generate();
if (api.errors.length) {
console.error(api.errorsToAscii());
console.error(api.linesToAscii());
console.log(api.displayAttributeErrors());
throw new Error(`ABNF grammar has errors`);
}

const grammarObj = api.toObject();
const parser = new apgLib.parser();
parser.ast = new apgLib.ast();
const id = apgLib.ids;

const domain = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.domain = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks.domain = domain;
const address = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.address = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks.address = address;
const statement = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.statement = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks.statement = statement;
const uri = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
if (!data.uri) {
data.uri = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
}
return ret;
};
parser.ast.callbacks.uri = uri;
const version = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.version = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks.version = version;
const chainId = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.chainId = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks['chain-id'] = chainId;
const nonce = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.nonce = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks.nonce = nonce;
const issuedAt = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.issuedAt = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks['issued-at'] = issuedAt;
const expirationTime = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.expirationTime = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks['expiration-time'] = expirationTime;
const notBefore = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.notBefore = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks['not-before'] = notBefore;
const requestId = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.requestId = apgLib.utils.charsToString(chars, phraseIndex, phraseLength);
}
return ret;
};
parser.ast.callbacks['request-id'] = requestId;
const resources = function (state, chars, phraseIndex, phraseLength, data) {
const ret = id.SEM_OK;
if (state === id.SEM_PRE) {
data.resources = apgLib.utils.charsToString(chars, phraseIndex, phraseLength).slice(2).split('\n- ');
}
return ret;
};
parser.ast.callbacks.resources = resources;

const result = parser.parse(grammarObj, 'sign-in-with-ethereum', msg);
if (!result.success) {
throw new Error(`Invalid message: ${JSON.stringify(result)}`);
}
const elements = {};
parser.ast.translate(elements);
for (const [key, value] of Object.entries(elements)) {
this[key] = value;
}
}
}
Loading

0 comments on commit 3a018b2

Please sign in to comment.